Bugs Fixed.
If the bugs are resolved, users will be able to operate the program without experiencing crashes.
Bugs Fixed: Special thanks to David for stress-testing the program last week. The following issues have been resolved:
My next step is to address any bugs that David may encounter during testing. I’ll focus on stabilizing the modules with outstanding issues and create documentation that explains the current status of the codebase and protocol for packaging.
|
@@ -20,3 +20,78 @@ Thank you for your interest in CASPER. Our packaged releases for Windows 10 and
|
|
| 20 |
CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
|
| 21 |
|
| 22 |
NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
|
| 21 |
|
| 22 |
NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
|
| 23 |
+
|
| 24 |
+
### Docker Installation (macOS)
|
| 25 |
+
|
| 26 |
+
#### Prerequisites
|
| 27 |
+
1. Install Docker Desktop for Mac
|
| 28 |
+
```bash
|
| 29 |
+
brew install --cask docker
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
2. Install XQuartz
|
| 33 |
+
```bash
|
| 34 |
+
brew install --cask xquartz
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
3. Configure XQuartz:
|
| 38 |
+
```bash
|
| 39 |
+
# Start XQuartz
|
| 40 |
+
open -a XQuartz
|
| 41 |
+
|
| 42 |
+
# In XQuartz Preferences → Security:
|
| 43 |
+
# - Check "Allow connections from network clients"
|
| 44 |
+
# - Restart XQuartz after changing settings
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
4. Set up X11 forwarding (run these commands each time before starting the app):
|
| 48 |
+
```bash
|
| 49 |
+
# Start XQuartz if not running
|
| 50 |
+
open -a XQuartz
|
| 51 |
+
|
| 52 |
+
# Get your IP address
|
| 53 |
+
export IP=$(ifconfig en0 | grep inet | awk '$1=="inet" {print $2}')
|
| 54 |
+
|
| 55 |
+
# Set up permissions (use your actual IP)
|
| 56 |
+
xhost + $IP
|
| 57 |
+
|
| 58 |
+
# Clean up any old containers
|
| 59 |
+
docker-compose down
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
5. Run CASPER:
|
| 63 |
+
```bash
|
| 64 |
+
# First time or after making changes:
|
| 65 |
+
docker-compose up --build
|
| 66 |
+
|
| 67 |
+
# Subsequent runs (without rebuilding):
|
| 68 |
+
docker-compose up
|
| 69 |
+
|
| 70 |
+
# Or run in background:
|
| 71 |
+
docker-compose up -d
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
#### Troubleshooting
|
| 75 |
+
- If the app doesn't start:
|
| 76 |
+
```bash
|
| 77 |
+
# Stop all containers
|
| 78 |
+
docker-compose down
|
| 79 |
+
|
| 80 |
+
# Remove old containers and images
|
| 81 |
+
docker system prune -f
|
| 82 |
+
|
| 83 |
+
# Restart XQuartz
|
| 84 |
+
killall Xquartz
|
| 85 |
+
open -a XQuartz
|
| 86 |
+
|
| 87 |
+
# Set up X11 again
|
| 88 |
+
xhost + localhost
|
| 89 |
+
|
| 90 |
+
# Try running again
|
| 91 |
+
docker-compose up --build
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
- If you still have issues:
|
| 95 |
+
- Make sure Docker Desktop is running
|
| 96 |
+
- Try restarting your computer
|
| 97 |
+
- Run `docker-compose logs` to see detailed error messages
|
|
@@ -1,30 +1,48 @@
|
|
|
|
|
|
|
|
| 1 |
beautifulsoup4==4.12.3
|
| 2 |
biopython==1.84
|
|
|
|
|
|
|
| 3 |
contourpy==1.3.0
|
| 4 |
cycler==0.12.1
|
| 5 |
darkdetect==0.7.1
|
|
|
|
| 6 |
fonttools==4.53.1
|
|
|
|
|
|
|
|
|
|
| 7 |
joblib==1.4.2
|
| 8 |
kiwisolver==1.4.7
|
| 9 |
lxml==5.3.0
|
|
|
|
| 10 |
matplotlib==3.9.2
|
|
|
|
| 11 |
mplcursors==0.5.3
|
| 12 |
numpy==2.1.1
|
| 13 |
packaging==24.1
|
| 14 |
pandas==2.2.2
|
| 15 |
pillow==10.4.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
pyparsing==3.1.4
|
| 17 |
PyQt6==6.7.1
|
| 18 |
PyQt6-Qt6==6.7.2
|
| 19 |
PyQt6_sip==13.8.0
|
| 20 |
pyqtdarktheme==2.1.0
|
|
|
|
| 21 |
python-dateutil==2.9.0.post0
|
| 22 |
python-dotenv==1.0.1
|
| 23 |
pytz==2024.1
|
| 24 |
PyYAML==6.0.2
|
|
|
|
| 25 |
scikit-learn==1.5.2
|
| 26 |
scipy==1.14.1
|
| 27 |
six==1.16.0
|
| 28 |
soupsieve==2.6
|
| 29 |
threadpoolctl==3.5.0
|
|
|
|
| 30 |
tzdata==2024.1
|
|
|
|
|
|
| 1 |
+
altgraph==0.17.4
|
| 2 |
+
astroid==3.3.5
|
| 3 |
beautifulsoup4==4.12.3
|
| 4 |
biopython==1.84
|
| 5 |
+
certifi==2024.8.30
|
| 6 |
+
charset-normalizer==3.4.0
|
| 7 |
contourpy==1.3.0
|
| 8 |
cycler==0.12.1
|
| 9 |
darkdetect==0.7.1
|
| 10 |
+
dill==0.3.9
|
| 11 |
fonttools==4.53.1
|
| 12 |
+
graphviz==0.20.3
|
| 13 |
+
idna==3.10
|
| 14 |
+
isort==5.13.2
|
| 15 |
joblib==1.4.2
|
| 16 |
kiwisolver==1.4.7
|
| 17 |
lxml==5.3.0
|
| 18 |
+
macholib==1.16.3
|
| 19 |
matplotlib==3.9.2
|
| 20 |
+
mccabe==0.7.0
|
| 21 |
mplcursors==0.5.3
|
| 22 |
numpy==2.1.1
|
| 23 |
packaging==24.1
|
| 24 |
pandas==2.2.2
|
| 25 |
pillow==10.4.0
|
| 26 |
+
platformdirs==4.3.6
|
| 27 |
+
pyinstaller==6.11.1
|
| 28 |
+
pyinstaller-hooks-contrib==2024.10
|
| 29 |
+
pylint==3.3.1
|
| 30 |
pyparsing==3.1.4
|
| 31 |
PyQt6==6.7.1
|
| 32 |
PyQt6-Qt6==6.7.2
|
| 33 |
PyQt6_sip==13.8.0
|
| 34 |
pyqtdarktheme==2.1.0
|
| 35 |
+
python-call-graph==2.1.2
|
| 36 |
python-dateutil==2.9.0.post0
|
| 37 |
python-dotenv==1.0.1
|
| 38 |
pytz==2024.1
|
| 39 |
PyYAML==6.0.2
|
| 40 |
+
requests==2.32.3
|
| 41 |
scikit-learn==1.5.2
|
| 42 |
scipy==1.14.1
|
| 43 |
six==1.16.0
|
| 44 |
soupsieve==2.6
|
| 45 |
threadpoolctl==3.5.0
|
| 46 |
+
tomlkit==0.13.2
|
| 47 |
tzdata==2024.1
|
| 48 |
+
urllib3==2.2.3
|
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
block_cipher = None
|
| 2 |
-
|
| 3 |
-
a = Analysis(['src/main.py'],
|
| 4 |
-
pathex=['src'],
|
| 5 |
-
datas=[
|
| 6 |
-
('assets', 'assets'),
|
| 7 |
-
('config', 'config'),
|
| 8 |
-
('logs', 'logs'),
|
| 9 |
-
('src', 'src'),
|
| 10 |
-
('genomeBrowserTemplate.html', '.'),
|
| 11 |
-
],
|
| 12 |
-
hiddenimports=[],
|
| 13 |
-
hookspath=[],
|
| 14 |
-
runtime_hooks=[],
|
| 15 |
-
excludes=[],
|
| 16 |
-
win_no_prefer_redirects=False,
|
| 17 |
-
win_private_assemblies=False,
|
| 18 |
-
cipher=block_cipher,
|
| 19 |
-
noarchive=False)
|
| 20 |
-
|
| 21 |
-
pyz = PYZ(a.pure, a.zipped_data,
|
| 22 |
-
cipher=block_cipher)
|
| 23 |
-
|
| 24 |
-
exe = EXE(pyz,
|
| 25 |
-
a.scripts,
|
| 26 |
-
[],
|
| 27 |
-
exclude_binaries=True,
|
| 28 |
-
name='CASPERapp',
|
| 29 |
-
debug=False,
|
| 30 |
-
bootloader_ignore_signals=False,
|
| 31 |
-
strip=False,
|
| 32 |
-
upx=True,
|
| 33 |
-
console=False,
|
| 34 |
-
disable_windowed_traceback=False,
|
| 35 |
-
target_arch=None,
|
| 36 |
-
codesign_identity=None,
|
| 37 |
-
entitlements_file=None,
|
| 38 |
-
icon='assets/CASPER_icon.icns')
|
| 39 |
-
|
| 40 |
-
coll = COLLECT(exe,
|
| 41 |
-
a.binaries,
|
| 42 |
-
a.zipfiles,
|
| 43 |
-
a.datas,
|
| 44 |
-
strip=False,
|
| 45 |
-
upx=True,
|
| 46 |
-
upx_exclude=[],
|
| 47 |
-
name='CASPERapp')
|
| 48 |
-
|
| 49 |
-
app = BUNDLE(coll,
|
| 50 |
-
name='CASPERapp.app',
|
| 51 |
-
icon='assets/CASPER_icon.icns',
|
| 52 |
-
version='2.0.1',
|
| 53 |
-
bundle_identifier=None)
|
| 54 |
-
|
| 55 |
-
# 1. Have the mac.spec in the app directory
|
| 56 |
-
# 2. pyinstaller mac.spec
|
| 57 |
-
# 3. mkdir -p dist/dmg
|
| 58 |
-
# 4. rm -r dist/dmg/*
|
| 59 |
-
# 5. Manual copy of the app into dist/dmg
|
| 60 |
-
# 6. create-dmg \
|
| 61 |
-
# --volname "CASPERapp" \
|
| 62 |
-
# --window-pos 200 120 \
|
| 63 |
-
# --window-size 600 300 \
|
| 64 |
-
# --icon-size 100 \
|
| 65 |
-
# --icon "CASPERapp.app" 175 120 \
|
| 66 |
-
# --hide-extension "CASPERapp.app" \
|
| 67 |
-
# --app-drop-link 425 120 \
|
| 68 |
-
# "dist/CASPERapp.dmg" \
|
| 69 |
-
# "dist/dmg/"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -4,6 +4,7 @@ from views.FindTargetsView import FindTargetsView
|
|
| 4 |
from PyQt6.QtWidgets import QMessageBox
|
| 5 |
from views.LoadingDialog import LoadingDialog
|
| 6 |
from PyQt6.QtWidgets import QApplication
|
|
|
|
| 7 |
|
| 8 |
class FindTargetsController:
|
| 9 |
def __init__(self, global_settings):
|
|
@@ -26,17 +27,28 @@ class FindTargetsController:
|
|
| 26 |
self.global_settings.logger.debug(f"FindTargetsController received new annotation file: {new_annotation_file}")
|
| 27 |
self._current_annotation_file = new_annotation_file
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
if
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
if self.
|
|
|
|
|
|
|
|
|
|
| 35 |
self._input_data['annotation_file'] = new_annotation_file
|
| 36 |
self._process_input_data(self._input_data)
|
| 37 |
|
| 38 |
except Exception as e:
|
| 39 |
self.global_settings.logger.error(f"Error handling annotation file change: {str(e)}")
|
|
|
|
| 40 |
|
| 41 |
def _connect_signals(self):
|
| 42 |
"""Connect view signals"""
|
|
@@ -93,22 +105,35 @@ class FindTargetsController:
|
|
| 93 |
QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
|
| 94 |
return
|
| 95 |
|
| 96 |
-
# Create loading dialog
|
| 97 |
-
|
|
|
|
| 98 |
loading_dialog.show()
|
| 99 |
loading_dialog.set_progress(0)
|
| 100 |
QApplication.processEvents()
|
| 101 |
|
| 102 |
try:
|
| 103 |
-
#
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
loading_dialog.set_message("Initializing view targets...", 25)
|
| 108 |
QApplication.processEvents()
|
| 109 |
|
| 110 |
if existing_tab:
|
| 111 |
-
view_targets_controller = main_window.tab_widgets['controllers'].get(
|
| 112 |
if view_targets_controller:
|
| 113 |
loading_dialog.set_message("Loading guides...", 50)
|
| 114 |
QApplication.processEvents()
|
|
@@ -124,7 +149,7 @@ class FindTargetsController:
|
|
| 124 |
# Switch to the existing tab
|
| 125 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 126 |
else:
|
| 127 |
-
self.logger.error("View Targets controller not found for existing tab")
|
| 128 |
else:
|
| 129 |
loading_dialog.set_message("Creating view targets...", 25)
|
| 130 |
QApplication.processEvents()
|
|
@@ -139,7 +164,7 @@ class FindTargetsController:
|
|
| 139 |
loading_dialog=loading_dialog
|
| 140 |
)
|
| 141 |
|
| 142 |
-
main_window.open_new_tab(
|
| 143 |
|
| 144 |
finally:
|
| 145 |
loading_dialog.close()
|
|
@@ -168,34 +193,55 @@ class FindTargetsController:
|
|
| 168 |
def open_view_targets_directly(self, input_data):
|
| 169 |
"""Open view targets directly for position-based searches"""
|
| 170 |
try:
|
| 171 |
-
#
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
| 180 |
|
| 181 |
-
#
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
input_data
|
| 185 |
-
input_data['endonuclease']
|
| 186 |
)
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
except Exception as e:
|
| 201 |
self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
|
|
|
|
| 4 |
from PyQt6.QtWidgets import QMessageBox
|
| 5 |
from views.LoadingDialog import LoadingDialog
|
| 6 |
from PyQt6.QtWidgets import QApplication
|
| 7 |
+
import os
|
| 8 |
|
| 9 |
class FindTargetsController:
|
| 10 |
def __init__(self, global_settings):
|
|
|
|
| 27 |
self.global_settings.logger.debug(f"FindTargetsController received new annotation file: {new_annotation_file}")
|
| 28 |
self._current_annotation_file = new_annotation_file
|
| 29 |
|
| 30 |
+
# Only process if we have a valid annotation file and input data
|
| 31 |
+
if new_annotation_file and self._input_data:
|
| 32 |
+
# Verify annotation file exists
|
| 33 |
+
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', new_annotation_file)
|
| 34 |
+
if not os.path.isfile(annotation_path):
|
| 35 |
+
# Try without GBFF subdirectory
|
| 36 |
+
annotation_path = os.path.join(self.global_settings.get_db_path(), new_annotation_file)
|
| 37 |
+
if not os.path.isfile(annotation_path):
|
| 38 |
+
self.logger.warning(f"Annotation file not found at {annotation_path}")
|
| 39 |
+
return
|
| 40 |
|
| 41 |
+
# Clear the current results
|
| 42 |
+
if self.view and hasattr(self.view, 'results_table'):
|
| 43 |
+
self.view.clear_results()
|
| 44 |
+
|
| 45 |
+
# Update input data with new annotation file
|
| 46 |
self._input_data['annotation_file'] = new_annotation_file
|
| 47 |
self._process_input_data(self._input_data)
|
| 48 |
|
| 49 |
except Exception as e:
|
| 50 |
self.global_settings.logger.error(f"Error handling annotation file change: {str(e)}")
|
| 51 |
+
# Don't raise the error since this is an event handler
|
| 52 |
|
| 53 |
def _connect_signals(self):
|
| 54 |
"""Connect view signals"""
|
|
|
|
| 105 |
QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
|
| 106 |
return
|
| 107 |
|
| 108 |
+
# Create loading dialog with the main window as parent
|
| 109 |
+
main_window = self.global_settings.main_window
|
| 110 |
+
loading_dialog = LoadingDialog(main_window.view)
|
| 111 |
loading_dialog.show()
|
| 112 |
loading_dialog.set_progress(0)
|
| 113 |
QApplication.processEvents()
|
| 114 |
|
| 115 |
try:
|
| 116 |
+
# Get the current Find Targets tab number
|
| 117 |
+
current_tab_index = main_window.view.tab_widget.currentIndex()
|
| 118 |
+
current_tab_title = main_window.view.tab_widget.tabText(current_tab_index)
|
| 119 |
+
|
| 120 |
+
# Extract number from Find Targets tab (if any)
|
| 121 |
+
view_targets_title = "View Targets"
|
| 122 |
+
if current_tab_title != "Find Targets":
|
| 123 |
+
try:
|
| 124 |
+
number = current_tab_title.split()[-1]
|
| 125 |
+
view_targets_title = f"View Targets {number}"
|
| 126 |
+
except (IndexError, ValueError):
|
| 127 |
+
pass
|
| 128 |
+
|
| 129 |
+
# Find existing View Targets tab with the same number
|
| 130 |
+
existing_tab = main_window.find_tab_by_title(view_targets_title)
|
| 131 |
|
| 132 |
loading_dialog.set_message("Initializing view targets...", 25)
|
| 133 |
QApplication.processEvents()
|
| 134 |
|
| 135 |
if existing_tab:
|
| 136 |
+
view_targets_controller = main_window.tab_widgets['controllers'].get(view_targets_title)
|
| 137 |
if view_targets_controller:
|
| 138 |
loading_dialog.set_message("Loading guides...", 50)
|
| 139 |
QApplication.processEvents()
|
|
|
|
| 149 |
# Switch to the existing tab
|
| 150 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 151 |
else:
|
| 152 |
+
self.logger.error(f"View Targets controller not found for existing tab {view_targets_title}")
|
| 153 |
else:
|
| 154 |
loading_dialog.set_message("Creating view targets...", 25)
|
| 155 |
QApplication.processEvents()
|
|
|
|
| 164 |
loading_dialog=loading_dialog
|
| 165 |
)
|
| 166 |
|
| 167 |
+
main_window.open_new_tab(view_targets_title, view_targets_controller)
|
| 168 |
|
| 169 |
finally:
|
| 170 |
loading_dialog.close()
|
|
|
|
| 193 |
def open_view_targets_directly(self, input_data):
|
| 194 |
"""Open view targets directly for position-based searches"""
|
| 195 |
try:
|
| 196 |
+
# Create loading dialog with the main window as parent
|
| 197 |
+
main_window = self.global_settings.main_window
|
| 198 |
+
loading_dialog = LoadingDialog(main_window.view)
|
| 199 |
+
loading_dialog.show()
|
| 200 |
+
loading_dialog.set_progress(0)
|
| 201 |
+
QApplication.processEvents()
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
loading_dialog.set_message("Finding targets...", 25)
|
| 205 |
+
QApplication.processEvents()
|
| 206 |
|
| 207 |
+
# Get targets using the model
|
| 208 |
+
targets = self.model.find_targets_by_position(
|
| 209 |
+
self.model._get_parser(self.model.get_cspr_file_path(input_data)),
|
| 210 |
+
input_data
|
|
|
|
| 211 |
)
|
| 212 |
|
| 213 |
+
if targets:
|
| 214 |
+
loading_dialog.set_message("Creating view targets...", 50)
|
| 215 |
+
QApplication.processEvents()
|
| 216 |
+
|
| 217 |
+
# Create view targets controller
|
| 218 |
+
view_targets_controller = self.global_settings.get_view_targets_window()
|
| 219 |
+
|
| 220 |
+
loading_dialog.set_message("Loading guides...", 75)
|
| 221 |
+
QApplication.processEvents()
|
| 222 |
+
|
| 223 |
+
# Load targets directly
|
| 224 |
+
view_targets_controller.load_targets(
|
| 225 |
+
targets,
|
| 226 |
+
input_data['organism'],
|
| 227 |
+
input_data['endonuclease']
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Open view targets tab
|
| 231 |
+
self.global_settings.main_window.open_new_tab(
|
| 232 |
+
"View Targets",
|
| 233 |
+
view_targets_controller
|
| 234 |
+
)
|
| 235 |
+
else:
|
| 236 |
+
QMessageBox.warning(
|
| 237 |
+
self.view,
|
| 238 |
+
"No Targets Found",
|
| 239 |
+
"No targets were found in the specified position range."
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
finally:
|
| 243 |
+
loading_dialog.close()
|
| 244 |
+
QApplication.processEvents()
|
| 245 |
|
| 246 |
except Exception as e:
|
| 247 |
self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
|
|
@@ -13,10 +13,18 @@ class GenerateLibraryController(QObject):
|
|
| 13 |
self.model = GenerateLibraryModel(global_settings)
|
| 14 |
self.view = GenerateLibraryView(global_settings)
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# Get CSPR file path
|
| 17 |
-
if selected_targets and len(selected_targets) > 0:
|
| 18 |
# Get organism name from the first target's chromosome
|
| 19 |
-
chrom = selected_targets[0].get('full_chromosome', '')
|
| 20 |
if chrom:
|
| 21 |
# Extract organism name from chromosome ID
|
| 22 |
org_name = chrom.split('.')[0]
|
|
@@ -25,7 +33,7 @@ class GenerateLibraryController(QObject):
|
|
| 25 |
|
| 26 |
# Get CSPR file path and initialize parser
|
| 27 |
org_files = self.model.get_organism_to_files()
|
| 28 |
-
endonuclease = selected_targets[0].get('endonuclease', '').lower()
|
| 29 |
if org_name in org_files and endonuclease in org_files[org_name]:
|
| 30 |
cspr_file = os.path.join(
|
| 31 |
self.global_settings.get_db_path(),
|
|
@@ -35,7 +43,7 @@ class GenerateLibraryController(QObject):
|
|
| 35 |
|
| 36 |
# Get guide data for each target
|
| 37 |
processed_targets = []
|
| 38 |
-
for target in selected_targets:
|
| 39 |
target_info = [{
|
| 40 |
'feature_id': target['feature_id'],
|
| 41 |
'feature_name': target['feature_name'],
|
|
@@ -63,11 +71,6 @@ class GenerateLibraryController(QObject):
|
|
| 63 |
self.selected_targets = processed_targets
|
| 64 |
else:
|
| 65 |
self.selected_targets = selected_targets
|
| 66 |
-
else:
|
| 67 |
-
self.selected_targets = selected_targets
|
| 68 |
-
|
| 69 |
-
# Log initialization
|
| 70 |
-
self.logger.debug(f"Initializing GenerateLibraryController with {len(selected_targets) if selected_targets else 0} targets")
|
| 71 |
|
| 72 |
self._connect_signals()
|
| 73 |
|
|
@@ -75,15 +78,12 @@ class GenerateLibraryController(QObject):
|
|
| 75 |
"""Connect view signals to controller methods"""
|
| 76 |
try:
|
| 77 |
self.view.submit_clicked.connect(self._handle_submit)
|
| 78 |
-
self.logger.debug("Connected GenerateLibraryView signals")
|
| 79 |
except Exception as e:
|
| 80 |
self.logger.error(f"Error connecting signals: {str(e)}")
|
| 81 |
|
| 82 |
def show(self):
|
| 83 |
"""Show the generate library window"""
|
| 84 |
try:
|
| 85 |
-
self.logger.debug("Showing GenerateLibraryView")
|
| 86 |
-
|
| 87 |
# Store reference to prevent garbage collection
|
| 88 |
self.global_settings.main_window._current_generate_library_controller = self
|
| 89 |
|
|
@@ -184,13 +184,17 @@ class GenerateLibraryController(QObject):
|
|
| 184 |
else:
|
| 185 |
raise ValueError(f"Could not find organism {org_name} in database")
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
# Generate library using processed targets
|
| 188 |
success = self.model.generate_library(
|
| 189 |
self.processed_targets if hasattr(self, 'processed_targets') else self.selected_targets,
|
| 190 |
settings
|
| 191 |
)
|
| 192 |
|
| 193 |
-
if success:
|
| 194 |
self.view.show_success("Library generated successfully!")
|
| 195 |
self.view.close()
|
| 196 |
|
|
@@ -221,9 +225,16 @@ class GenerateLibraryController(QObject):
|
|
| 221 |
|
| 222 |
if settings.get('find_off_targets'):
|
| 223 |
max_score = settings.get('max_off_target_score')
|
| 224 |
-
if max_score is None or not 0
|
| 225 |
-
raise ValueError("Maximum off-target score must be between 0 and 0.5")
|
| 226 |
|
| 227 |
except Exception as e:
|
| 228 |
self.logger.error(f"Settings validation error: {str(e)}")
|
| 229 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
self.model = GenerateLibraryModel(global_settings)
|
| 14 |
self.view = GenerateLibraryView(global_settings)
|
| 15 |
|
| 16 |
+
# Get selected targets from global settings if not provided
|
| 17 |
+
if selected_targets is None and hasattr(self.global_settings, '_current_selected_targets'):
|
| 18 |
+
selected_targets = self.global_settings._current_selected_targets
|
| 19 |
+
|
| 20 |
+
self.selected_targets = selected_targets or []
|
| 21 |
+
|
| 22 |
+
self.view.ledFileName.setText('eck_12_spCas9_lib')
|
| 23 |
+
|
| 24 |
# Get CSPR file path
|
| 25 |
+
if self.selected_targets and len(self.selected_targets) > 0:
|
| 26 |
# Get organism name from the first target's chromosome
|
| 27 |
+
chrom = self.selected_targets[0].get('full_chromosome', '')
|
| 28 |
if chrom:
|
| 29 |
# Extract organism name from chromosome ID
|
| 30 |
org_name = chrom.split('.')[0]
|
|
|
|
| 33 |
|
| 34 |
# Get CSPR file path and initialize parser
|
| 35 |
org_files = self.model.get_organism_to_files()
|
| 36 |
+
endonuclease = self.selected_targets[0].get('endonuclease', '').lower()
|
| 37 |
if org_name in org_files and endonuclease in org_files[org_name]:
|
| 38 |
cspr_file = os.path.join(
|
| 39 |
self.global_settings.get_db_path(),
|
|
|
|
| 43 |
|
| 44 |
# Get guide data for each target
|
| 45 |
processed_targets = []
|
| 46 |
+
for target in self.selected_targets:
|
| 47 |
target_info = [{
|
| 48 |
'feature_id': target['feature_id'],
|
| 49 |
'feature_name': target['feature_name'],
|
|
|
|
| 71 |
self.selected_targets = processed_targets
|
| 72 |
else:
|
| 73 |
self.selected_targets = selected_targets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
self._connect_signals()
|
| 76 |
|
|
|
|
| 78 |
"""Connect view signals to controller methods"""
|
| 79 |
try:
|
| 80 |
self.view.submit_clicked.connect(self._handle_submit)
|
|
|
|
| 81 |
except Exception as e:
|
| 82 |
self.logger.error(f"Error connecting signals: {str(e)}")
|
| 83 |
|
| 84 |
def show(self):
|
| 85 |
"""Show the generate library window"""
|
| 86 |
try:
|
|
|
|
|
|
|
| 87 |
# Store reference to prevent garbage collection
|
| 88 |
self.global_settings.main_window._current_generate_library_controller = self
|
| 89 |
|
|
|
|
| 184 |
else:
|
| 185 |
raise ValueError(f"Could not find organism {org_name} in database")
|
| 186 |
|
| 187 |
+
# Connect to model's progress signal if off-target analysis is enabled
|
| 188 |
+
if settings.get('find_off_targets'):
|
| 189 |
+
self.model.progress_updated.connect(self._handle_progress)
|
| 190 |
+
|
| 191 |
# Generate library using processed targets
|
| 192 |
success = self.model.generate_library(
|
| 193 |
self.processed_targets if hasattr(self, 'processed_targets') else self.selected_targets,
|
| 194 |
settings
|
| 195 |
)
|
| 196 |
|
| 197 |
+
if success and not settings.get('find_off_targets'):
|
| 198 |
self.view.show_success("Library generated successfully!")
|
| 199 |
self.view.close()
|
| 200 |
|
|
|
|
| 225 |
|
| 226 |
if settings.get('find_off_targets'):
|
| 227 |
max_score = settings.get('max_off_target_score')
|
| 228 |
+
if max_score is None or not 0 <= max_score <= 0.5:
|
| 229 |
+
raise ValueError("Maximum off-target score must be between 0 and 0.5 inclusive")
|
| 230 |
|
| 231 |
except Exception as e:
|
| 232 |
self.logger.error(f"Settings validation error: {str(e)}")
|
| 233 |
raise
|
| 234 |
+
|
| 235 |
+
def _handle_progress(self, value):
|
| 236 |
+
"""Handle progress updates from model"""
|
| 237 |
+
try:
|
| 238 |
+
self.view.progBar.setValue(value)
|
| 239 |
+
except Exception as e:
|
| 240 |
+
self.logger.error(f"Error updating progress: {str(e)}")
|
|
@@ -7,69 +7,121 @@ from models.DatabaseManager import FileChangeType
|
|
| 7 |
import time
|
| 8 |
from views.LoadingDialog import LoadingDialog
|
| 9 |
from PyQt6.QtWidgets import QApplication
|
|
|
|
| 10 |
|
| 11 |
class HomeWindowController:
|
| 12 |
def __init__(self, global_settings):
|
| 13 |
-
self.
|
| 14 |
-
self.logger =
|
|
|
|
|
|
|
| 15 |
try:
|
| 16 |
-
self.
|
| 17 |
-
self.
|
| 18 |
-
self.
|
| 19 |
-
self.
|
| 20 |
-
self.model.load_data()
|
| 21 |
-
self.global_settings.db_manager.db_files_changed.connect(self._handle_db_files_changed)
|
| 22 |
-
self.global_settings.db_manager.db_validation_changed.connect(self._handle_db_validation_changed)
|
| 23 |
-
self.global_settings.db_manager.db_state_changed.connect(self._handle_db_state_changed)
|
| 24 |
except Exception as e:
|
| 25 |
-
show_error(self.
|
| 26 |
|
| 27 |
-
def
|
|
|
|
| 28 |
try:
|
| 29 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
self.handle_search_type_change()
|
| 31 |
except Exception as e:
|
| 32 |
-
show_error(self.
|
| 33 |
|
| 34 |
-
def
|
| 35 |
-
"""
|
| 36 |
try:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
organism_to_endonuclease = self.model.get_organism_to_endonuclease()
|
| 40 |
annotation_files = self.model.get_annotation_files()
|
| 41 |
|
| 42 |
-
# Update
|
| 43 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
# Update
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
self.view.update_combo_box_annotation_files(annotation_files)
|
| 50 |
-
|
| 51 |
except Exception as e:
|
| 52 |
-
|
| 53 |
|
| 54 |
-
def
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
self.logger.debug(f"Updating endonuclease combo box for organism {selected_organism} with endonuclease: {endonuclease} in Main window")
|
| 58 |
-
self.view.update_combo_box_endonuclease(endonuclease)
|
| 59 |
|
| 60 |
-
def
|
| 61 |
try:
|
| 62 |
# grpNavigationMenu
|
| 63 |
-
self.view.push_button_new_genome.clicked.connect(self.
|
| 64 |
-
self.view.push_button_new_endonuclease.clicked.connect(self.
|
| 65 |
-
self.view.push_button_multitargeting_analysis.clicked.connect(self.
|
| 66 |
-
self.view.push_button_population_analysis.clicked.connect(self.
|
| 67 |
|
| 68 |
# grpStep1
|
| 69 |
-
self.view.combo_box_organism.currentIndexChanged.connect(self.
|
| 70 |
|
| 71 |
# grpStep2
|
| 72 |
-
self.view.push_button_ncbi_file_search.clicked.connect(self.
|
| 73 |
|
| 74 |
# grpStep3
|
| 75 |
self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
|
|
@@ -79,40 +131,122 @@ class HomeWindowController:
|
|
| 79 |
|
| 80 |
# Add connection for annotation file changes
|
| 81 |
self.view.combo_box_local_annotation_files.currentTextChanged.connect(self._on_annotation_file_changed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
except Exception as e:
|
| 83 |
-
show_error(self.
|
| 84 |
-
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
"""Process input data and direct to appropriate view"""
|
| 89 |
try:
|
| 90 |
-
|
|
|
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
except Exception as e:
|
| 115 |
-
|
| 116 |
|
| 117 |
def open_view_targets(self, input_data):
|
| 118 |
try:
|
|
@@ -124,7 +258,7 @@ class HomeWindowController:
|
|
| 124 |
|
| 125 |
try:
|
| 126 |
# Create find targets controller to use its model
|
| 127 |
-
find_targets_controller = self.
|
| 128 |
|
| 129 |
# For position searches, handle each query separately
|
| 130 |
if input_data['search_type'] == 'position':
|
|
@@ -164,7 +298,7 @@ class HomeWindowController:
|
|
| 164 |
QApplication.processEvents()
|
| 165 |
|
| 166 |
# Close existing View Targets tab if it exists
|
| 167 |
-
main_window = self.
|
| 168 |
existing_tab = main_window.find_tab_by_title("View Targets")
|
| 169 |
if existing_tab:
|
| 170 |
tab_index = main_window.view.tab_widget.indexOf(existing_tab)
|
|
@@ -174,7 +308,7 @@ class HomeWindowController:
|
|
| 174 |
# Create view targets controller
|
| 175 |
loading_dialog.set_message("Creating view targets...", 90)
|
| 176 |
QApplication.processEvents()
|
| 177 |
-
view_targets_controller = self.
|
| 178 |
|
| 179 |
view_targets_controller.load_guides(
|
| 180 |
targets,
|
|
@@ -200,10 +334,10 @@ class HomeWindowController:
|
|
| 200 |
loading_dialog.close()
|
| 201 |
|
| 202 |
except Exception as e:
|
| 203 |
-
self.
|
| 204 |
-
show_error(self.
|
| 205 |
|
| 206 |
-
def
|
| 207 |
"""Open find targets module for non-position searches"""
|
| 208 |
try:
|
| 209 |
# Show loading dialog
|
|
@@ -213,71 +347,112 @@ class HomeWindowController:
|
|
| 213 |
QApplication.processEvents()
|
| 214 |
|
| 215 |
try:
|
| 216 |
-
#
|
| 217 |
-
main_window = self.
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
loading_dialog.set_progress(40)
|
| 225 |
|
| 226 |
# Create new find targets controller and load data
|
| 227 |
-
find_targets_controller = self.
|
| 228 |
input_data = self.view.get_find_targets_input()
|
| 229 |
loading_dialog.set_progress(60)
|
| 230 |
|
| 231 |
find_targets_controller.find_targets(input_data)
|
| 232 |
loading_dialog.set_progress(80)
|
| 233 |
|
| 234 |
-
# Open new Find Targets tab
|
| 235 |
-
|
|
|
|
| 236 |
loading_dialog.set_progress(100)
|
| 237 |
|
| 238 |
finally:
|
| 239 |
loading_dialog.close()
|
| 240 |
|
| 241 |
except Exception as e:
|
| 242 |
-
show_error(self.
|
| 243 |
|
| 244 |
-
def
|
| 245 |
-
# Implementation for toggling annotation
|
| 246 |
-
pass
|
| 247 |
-
|
| 248 |
-
def open_new_genome_module(self):
|
| 249 |
try:
|
| 250 |
-
main_window = self.
|
| 251 |
existing_tab = main_window.find_tab_by_title("New Genome")
|
| 252 |
|
| 253 |
if existing_tab:
|
| 254 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 255 |
main_window._resize_for_tab("New Genome")
|
| 256 |
else:
|
| 257 |
-
new_genome_controller = self.
|
| 258 |
main_window.open_new_tab("New Genome", new_genome_controller)
|
| 259 |
except Exception as e:
|
| 260 |
-
show_error(self.
|
| 261 |
|
| 262 |
-
def
|
| 263 |
try:
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
except Exception as e:
|
| 273 |
-
show_error(self.
|
| 274 |
|
| 275 |
-
def
|
| 276 |
try:
|
| 277 |
start_time = time.time()
|
| 278 |
self.logger.debug("Starting multitargeting analysis module launch")
|
| 279 |
|
| 280 |
-
main_window = self.
|
| 281 |
existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
|
| 282 |
|
| 283 |
tab_check_time = time.time()
|
|
@@ -289,7 +464,7 @@ class HomeWindowController:
|
|
| 289 |
self.logger.debug(f"Switched to existing tab: {time.time() - tab_check_time:.2f} seconds")
|
| 290 |
else:
|
| 291 |
controller_start = time.time()
|
| 292 |
-
multitargeting_controller = self.
|
| 293 |
self.logger.debug(f"Controller creation took: {time.time() - controller_start:.2f} seconds")
|
| 294 |
|
| 295 |
tab_open_start = time.time()
|
|
@@ -298,119 +473,56 @@ class HomeWindowController:
|
|
| 298 |
|
| 299 |
self.logger.debug(f"Total multitargeting module launch took: {time.time() - start_time:.2f} seconds")
|
| 300 |
except Exception as e:
|
| 301 |
-
show_error(self.
|
| 302 |
|
| 303 |
-
def
|
| 304 |
try:
|
| 305 |
-
main_window = self.
|
| 306 |
existing_tab = main_window.find_tab_by_title("Population Analysis")
|
| 307 |
if existing_tab:
|
| 308 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 309 |
main_window._resize_for_tab("Population Analysis")
|
| 310 |
else:
|
| 311 |
-
population_analysis_controller = self.
|
| 312 |
main_window.open_new_tab("Population Analysis", population_analysis_controller)
|
| 313 |
except Exception as e:
|
| 314 |
-
show_error(self.
|
| 315 |
-
|
| 316 |
-
def launch_populate_fna_files(self):
|
| 317 |
-
# Implementation for launching populate FNA files
|
| 318 |
-
pass
|
| 319 |
|
| 320 |
-
def
|
| 321 |
try:
|
| 322 |
-
ncbi_controller = self.
|
| 323 |
-
self.
|
| 324 |
except Exception as e:
|
| 325 |
-
show_error(self.
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
# Reload model data if necessary
|
| 331 |
-
self.model.update_for_file_changes(changes)
|
| 332 |
-
|
| 333 |
-
# Update UI if needed
|
| 334 |
-
if (FileChangeType.CSPR_ADDED in changes or
|
| 335 |
-
FileChangeType.CSPR_REMOVED in changes):
|
| 336 |
-
# Update both organism and endonuclease combo boxes
|
| 337 |
-
organism_to_endonuclease = self.model.get_organism_to_endonuclease()
|
| 338 |
-
self.view.update_combo_box_organism(sorted(organism_to_endonuclease.keys()))
|
| 339 |
-
self.update_combo_box_endonuclease()
|
| 340 |
-
|
| 341 |
-
if (FileChangeType.GBFF_ADDED in changes or
|
| 342 |
-
FileChangeType.GBFF_REMOVED in changes):
|
| 343 |
-
self.view.update_combo_box_annotation_files(self.model.get_annotation_files())
|
| 344 |
-
|
| 345 |
-
except Exception as e:
|
| 346 |
-
show_error(self.global_settings, "Error handling database changes", str(e))
|
| 347 |
-
|
| 348 |
-
def _handle_db_validation_changed(self, is_valid, message):
|
| 349 |
-
"""Handle database validation state changes"""
|
| 350 |
-
try:
|
| 351 |
-
if not is_valid:
|
| 352 |
-
self.view.show_warning("Database Warning", message)
|
| 353 |
-
|
| 354 |
-
# Update UI elements based on validation state
|
| 355 |
-
self.view.push_button_find_view_targets.setEnabled(is_valid)
|
| 356 |
-
self.view.push_button_multitargeting_analysis.setEnabled(is_valid)
|
| 357 |
-
self.view.push_button_population_analysis.setEnabled(is_valid)
|
| 358 |
-
|
| 359 |
-
# Log the validation state change
|
| 360 |
-
self.logger.debug(f"Database validation state changed to: {is_valid}")
|
| 361 |
-
|
| 362 |
-
except Exception as e:
|
| 363 |
-
self.logger.error(f"Error handling database validation change: {str(e)}")
|
| 364 |
-
|
| 365 |
-
def _handle_db_state_changed(self, is_valid, message, changes):
|
| 366 |
-
"""Handle database state changes"""
|
| 367 |
try:
|
| 368 |
-
|
| 369 |
-
show_error(self.global_settings, "Database Warning", message)
|
| 370 |
-
return
|
| 371 |
-
|
| 372 |
-
# Always reload model data when database state changes
|
| 373 |
-
self.model.load_data()
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
except Exception as e:
|
| 382 |
-
show_error(self.
|
| 383 |
|
| 384 |
-
def _check_and_update_home_tab(self, index):
|
| 385 |
-
if self.global_settings.main_window.view.tab_widget.tabText(index) == "Home":
|
| 386 |
-
self.load_combo_box_data()
|
| 387 |
-
# Disconnect after updating to avoid unnecessary updates
|
| 388 |
-
self.global_settings.main_window.view.tab_widget.currentChanged.disconnect(self._check_and_update_home_tab)
|
| 389 |
-
|
| 390 |
-
def get_organism_to_endonuclease(self):
|
| 391 |
-
return self.model.get_organism_to_endonuclease()
|
| 392 |
-
|
| 393 |
-
def get_annotation_files(self):
|
| 394 |
-
return self.model.get_annotation_files()
|
| 395 |
-
|
| 396 |
-
def get_annotation_file(self):
|
| 397 |
-
return self.view.get_annotation_file()
|
| 398 |
-
|
| 399 |
-
def _on_annotation_file_changed(self, new_file):
|
| 400 |
-
"""Handle changes to the annotation file selection"""
|
| 401 |
-
self.global_settings.set_current_annotation_file(new_file)
|
| 402 |
-
|
| 403 |
-
def handle_search_type_change(self):
|
| 404 |
-
"""Update UI elements based on search type"""
|
| 405 |
-
try:
|
| 406 |
-
search_type = self.view.get_search_type()
|
| 407 |
-
|
| 408 |
-
# Update button text
|
| 409 |
-
if search_type in ['position', 'sequence']:
|
| 410 |
-
self.view.push_button_find_view_targets.setText("View Targets")
|
| 411 |
-
else: # 'feature'
|
| 412 |
-
self.view.push_button_find_view_targets.setText("Find Targets")
|
| 413 |
-
|
| 414 |
-
except Exception as e:
|
| 415 |
-
self.logger.error(f"Error updating search type UI: {str(e)}")
|
| 416 |
|
|
|
|
| 7 |
import time
|
| 8 |
from views.LoadingDialog import LoadingDialog
|
| 9 |
from PyQt6.QtWidgets import QApplication
|
| 10 |
+
from PyQt6.QtCore import Qt, QSize
|
| 11 |
|
| 12 |
class HomeWindowController:
|
| 13 |
def __init__(self, global_settings):
|
| 14 |
+
self.settings = global_settings
|
| 15 |
+
self.logger = self.settings.get_logger()
|
| 16 |
+
self.is_active = True
|
| 17 |
+
|
| 18 |
try:
|
| 19 |
+
self.view = HomeWindowView(self.settings)
|
| 20 |
+
self.model = HomeWindowModel(self.settings)
|
| 21 |
+
self._setup_connections()
|
| 22 |
+
self._init_ui()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
except Exception as e:
|
| 24 |
+
show_error(self.settings, "Error initializing HomeWindowController", str(e))
|
| 25 |
|
| 26 |
+
def deactivate(self):
|
| 27 |
+
"""Cleanup controller when deactivated"""
|
| 28 |
try:
|
| 29 |
+
self.is_active = False
|
| 30 |
+
if hasattr(self, 'view'):
|
| 31 |
+
# Safely disconnect signals
|
| 32 |
+
try:
|
| 33 |
+
self.settings.db_manager.db_state_changed.disconnect(self._on_db_state_changed)
|
| 34 |
+
except (TypeError, RuntimeError):
|
| 35 |
+
# Signal wasn't connected or already disconnected
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
self.settings.db_manager.db_files_changed.disconnect(self._on_db_files_changed)
|
| 40 |
+
except (TypeError, RuntimeError):
|
| 41 |
+
# Signal wasn't connected or already disconnected
|
| 42 |
+
pass
|
| 43 |
+
|
| 44 |
+
# Delete view reference
|
| 45 |
+
delattr(self, 'view')
|
| 46 |
+
|
| 47 |
+
except Exception as e:
|
| 48 |
+
self.logger.error(f"Error in deactivate: {str(e)}")
|
| 49 |
+
|
| 50 |
+
def _init_ui(self):
|
| 51 |
+
"""Initialize UI with current data"""
|
| 52 |
+
try:
|
| 53 |
+
self._update_ui_with_model_data()
|
| 54 |
self.handle_search_type_change()
|
| 55 |
except Exception as e:
|
| 56 |
+
show_error(self.settings, "Error initializing UI in HomeWindowController", str(e))
|
| 57 |
|
| 58 |
+
def _update_ui_with_model_data(self):
|
| 59 |
+
"""Update all UI elements with current model data"""
|
| 60 |
try:
|
| 61 |
+
# Get all required data at once
|
| 62 |
+
organism_data = self.model.get_organism_to_endonuclease()
|
|
|
|
| 63 |
annotation_files = self.model.get_annotation_files()
|
| 64 |
|
| 65 |
+
# Update UI elements
|
| 66 |
+
self._update_organism_selection(organism_data)
|
| 67 |
+
self._update_annotation_files(annotation_files)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
show_error(self.settings, "Error updating UI with model data", str(e))
|
| 70 |
+
|
| 71 |
+
def _update_organism_selection(self, organism_data, preserve_selection=True):
|
| 72 |
+
"""Update organism and its dependent endonuclease selection"""
|
| 73 |
+
try:
|
| 74 |
+
# Store current selection if needed
|
| 75 |
+
current_organism = self.view.combo_box_organism.currentText() if preserve_selection else ""
|
| 76 |
|
| 77 |
+
# Block signals during update
|
| 78 |
+
with self._block_signals(self.view.combo_box_organism):
|
| 79 |
+
# Update organisms
|
| 80 |
+
self.view.update_combo_box_organism(sorted(organism_data.keys()))
|
| 81 |
+
|
| 82 |
+
# Restore or select first item
|
| 83 |
+
if preserve_selection and current_organism in organism_data:
|
| 84 |
+
self.view.combo_box_organism.setCurrentText(current_organism)
|
| 85 |
|
| 86 |
+
# Update endonuclease based on current organism
|
| 87 |
+
self._update_endonuclease_for_organism(organism_data)
|
| 88 |
+
except Exception as e:
|
| 89 |
+
self.logger.error(f"Error updating organism selection: {str(e)}")
|
| 90 |
+
|
| 91 |
+
def _update_endonuclease_for_organism(self, organism_data):
|
| 92 |
+
"""Update endonuclease combo box based on current organism"""
|
| 93 |
+
try:
|
| 94 |
+
selected_organism = self.view.combo_box_organism.currentText()
|
| 95 |
+
endonucleases = organism_data.get(selected_organism, [])
|
| 96 |
+
self.logger.debug(f"Updating endonuclease combo box for organism {selected_organism} with endonuclease: {endonucleases} in Main window")
|
| 97 |
+
self.view.update_combo_box_endonuclease(endonucleases)
|
| 98 |
+
except Exception as e:
|
| 99 |
+
self.logger.error(f"Error updating endonuclease selection: {str(e)}")
|
| 100 |
+
|
| 101 |
+
def _update_annotation_files(self, annotation_files):
|
| 102 |
+
"""Update annotation files combo box"""
|
| 103 |
+
try:
|
| 104 |
self.view.update_combo_box_annotation_files(annotation_files)
|
|
|
|
| 105 |
except Exception as e:
|
| 106 |
+
self.logger.error(f"Error updating annotation files: {str(e)}")
|
| 107 |
|
| 108 |
+
def _on_organism_changed(self, _):
|
| 109 |
+
"""Handle organism combo box changes"""
|
| 110 |
+
self._update_endonuclease_for_organism(self.model.get_organism_to_endonuclease())
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
def _setup_connections(self):
|
| 113 |
try:
|
| 114 |
# grpNavigationMenu
|
| 115 |
+
self.view.push_button_new_genome.clicked.connect(self.open_new_genome)
|
| 116 |
+
self.view.push_button_new_endonuclease.clicked.connect(self.open_new_endonuclease)
|
| 117 |
+
self.view.push_button_multitargeting_analysis.clicked.connect(self.open_multitargeting_analysis)
|
| 118 |
+
self.view.push_button_population_analysis.clicked.connect(self.open_population_analysis)
|
| 119 |
|
| 120 |
# grpStep1
|
| 121 |
+
self.view.combo_box_organism.currentIndexChanged.connect(self._on_organism_changed)
|
| 122 |
|
| 123 |
# grpStep2
|
| 124 |
+
self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi)
|
| 125 |
|
| 126 |
# grpStep3
|
| 127 |
self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
|
|
|
|
| 131 |
|
| 132 |
# Add connection for annotation file changes
|
| 133 |
self.view.combo_box_local_annotation_files.currentTextChanged.connect(self._on_annotation_file_changed)
|
| 134 |
+
|
| 135 |
+
# Add connections for database changes
|
| 136 |
+
self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
|
| 137 |
+
self.settings.db_manager.db_state_changed.connect(self._on_db_state_changed)
|
| 138 |
+
self.settings.db_manager.db_files_changed.connect(self._on_db_files_changed)
|
| 139 |
+
|
| 140 |
except Exception as e:
|
| 141 |
+
show_error(self.settings, "Error setting up connections in HomeWindowController", str(e))
|
|
|
|
| 142 |
|
| 143 |
+
def _on_db_files_changed(self, changes):
|
| 144 |
+
"""Handle database file changes"""
|
|
|
|
| 145 |
try:
|
| 146 |
+
# Reload model data if necessary
|
| 147 |
+
self.model.update_for_file_changes(changes)
|
| 148 |
|
| 149 |
+
# Update UI based on change type
|
| 150 |
+
if self._should_update_organisms(changes):
|
| 151 |
+
self._update_organism_selection(self.model.get_organism_to_endonuclease())
|
| 152 |
+
|
| 153 |
+
if self._should_update_annotations(changes):
|
| 154 |
+
self._update_annotation_files(self.model.get_annotation_files())
|
| 155 |
+
|
| 156 |
+
except Exception as e:
|
| 157 |
+
show_error(self.settings, "Error handling database changes", str(e))
|
| 158 |
+
|
| 159 |
+
@staticmethod
|
| 160 |
+
def _should_update_organisms(changes):
|
| 161 |
+
"""Check if organisms need to be updated based on changes"""
|
| 162 |
+
return (FileChangeType.CSPR_ADDED in changes or
|
| 163 |
+
FileChangeType.CSPR_REMOVED in changes)
|
| 164 |
+
|
| 165 |
+
@staticmethod
|
| 166 |
+
def _should_update_annotations(changes):
|
| 167 |
+
"""Check if annotations need to be updated based on changes"""
|
| 168 |
+
return (FileChangeType.GBFF_ADDED in changes or
|
| 169 |
+
FileChangeType.GBFF_REMOVED in changes)
|
| 170 |
+
|
| 171 |
+
class _block_signals:
|
| 172 |
+
"""Context manager for blocking Qt signals"""
|
| 173 |
+
def __init__(self, widget):
|
| 174 |
+
self.widget = widget
|
| 175 |
+
|
| 176 |
+
def __enter__(self):
|
| 177 |
+
self.widget.blockSignals(True)
|
| 178 |
+
return self.widget
|
| 179 |
+
|
| 180 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 181 |
+
self.widget.blockSignals(False)
|
| 182 |
+
|
| 183 |
+
def refresh_data(self):
|
| 184 |
+
"""Refresh all data and update UI"""
|
| 185 |
+
try:
|
| 186 |
+
if not self.is_active or not hasattr(self, 'view'):
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
self.logger.debug("Refreshing home window data")
|
| 190 |
+
self.model.load_data()
|
| 191 |
+
self._update_ui_with_model_data()
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
self.logger.error(f"Error refreshing home window data: {str(e)}")
|
| 195 |
+
|
| 196 |
+
def _on_db_state_changed(self, is_valid, message, changes):
|
| 197 |
+
"""Handle database state changes"""
|
| 198 |
+
try:
|
| 199 |
+
if not self.is_active or not hasattr(self, 'view'):
|
| 200 |
+
return
|
| 201 |
|
| 202 |
+
self.logger.debug(f"Database state changed - Valid: {is_valid}, Message: {message}")
|
| 203 |
+
if is_valid:
|
| 204 |
+
self.refresh_data()
|
| 205 |
+
except Exception as e:
|
| 206 |
+
self.logger.error(f"Error handling database state change: {str(e)}")
|
| 207 |
+
|
| 208 |
+
def _check_and_update_home_tab(self, index):
|
| 209 |
+
if self.settings.main_window.view.tab_widget.tabText(index) == "Home":
|
| 210 |
+
self.load_combo_box_data()
|
| 211 |
+
# Disconnect after updating to avoid unnecessary updates
|
| 212 |
+
self.settings.main_window.view.tab_widget.currentChanged.disconnect(self._check_and_update_home_tab)
|
| 213 |
+
|
| 214 |
+
def get_organism_to_endonuclease(self):
|
| 215 |
+
return self.model.get_organism_to_endonuclease()
|
| 216 |
+
|
| 217 |
+
def get_annotation_files(self):
|
| 218 |
+
return self.model.get_annotation_files()
|
| 219 |
+
|
| 220 |
+
def get_annotation_file(self):
|
| 221 |
+
return self.view.get_annotation_file()
|
| 222 |
+
|
| 223 |
+
def _on_annotation_file_changed(self, new_file):
|
| 224 |
+
"""Handle changes to the annotation file selection"""
|
| 225 |
+
self.logger.debug(f"Current annotation file changed to: {new_file}")
|
| 226 |
+
self.settings.set_current_annotation_file(new_file)
|
| 227 |
+
|
| 228 |
+
def handle_search_type_change(self):
|
| 229 |
+
"""Update UI elements based on search type"""
|
| 230 |
+
try:
|
| 231 |
+
search_type = self.view.get_search_type()
|
| 232 |
+
|
| 233 |
+
# Update button text
|
| 234 |
+
if search_type in ['position', 'sequence']:
|
| 235 |
+
self.view.push_button_find_view_targets.setText("View Targets")
|
| 236 |
+
else: # 'feature'
|
| 237 |
+
self.view.push_button_find_view_targets.setText("Find Targets")
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
self.logger.error(f"Error updating search type UI: {str(e)}")
|
| 241 |
+
|
| 242 |
+
def _on_db_validation_changed(self, is_valid, message):
|
| 243 |
+
"""Handle database validation changes"""
|
| 244 |
+
try:
|
| 245 |
+
if self.is_active and hasattr(self, 'view'):
|
| 246 |
+
if is_valid:
|
| 247 |
+
self.refresh_data()
|
| 248 |
except Exception as e:
|
| 249 |
+
self.logger.error(f"Error handling database validation change: {str(e)}")
|
| 250 |
|
| 251 |
def open_view_targets(self, input_data):
|
| 252 |
try:
|
|
|
|
| 258 |
|
| 259 |
try:
|
| 260 |
# Create find targets controller to use its model
|
| 261 |
+
find_targets_controller = self.settings.get_find_targets_window()
|
| 262 |
|
| 263 |
# For position searches, handle each query separately
|
| 264 |
if input_data['search_type'] == 'position':
|
|
|
|
| 298 |
QApplication.processEvents()
|
| 299 |
|
| 300 |
# Close existing View Targets tab if it exists
|
| 301 |
+
main_window = self.settings.main_window
|
| 302 |
existing_tab = main_window.find_tab_by_title("View Targets")
|
| 303 |
if existing_tab:
|
| 304 |
tab_index = main_window.view.tab_widget.indexOf(existing_tab)
|
|
|
|
| 308 |
# Create view targets controller
|
| 309 |
loading_dialog.set_message("Creating view targets...", 90)
|
| 310 |
QApplication.processEvents()
|
| 311 |
+
view_targets_controller = self.settings.get_view_targets_window()
|
| 312 |
|
| 313 |
view_targets_controller.load_guides(
|
| 314 |
targets,
|
|
|
|
| 334 |
loading_dialog.close()
|
| 335 |
|
| 336 |
except Exception as e:
|
| 337 |
+
self.settings.logger.error(f"Error opening view targets directly: {str(e)}")
|
| 338 |
+
show_error(self.settings, "Error", f"Could not open view targets: {str(e)}")
|
| 339 |
|
| 340 |
+
def open_find_targets(self):
|
| 341 |
"""Open find targets module for non-position searches"""
|
| 342 |
try:
|
| 343 |
# Show loading dialog
|
|
|
|
| 347 |
QApplication.processEvents()
|
| 348 |
|
| 349 |
try:
|
| 350 |
+
# Find all existing Find Targets tabs
|
| 351 |
+
main_window = self.settings.main_window
|
| 352 |
+
existing_tabs = []
|
| 353 |
+
tab_numbers = []
|
| 354 |
+
|
| 355 |
+
# Get all tab titles
|
| 356 |
+
for i in range(main_window.view.tab_widget.count()):
|
| 357 |
+
tab_title = main_window.view.tab_widget.tabText(i)
|
| 358 |
+
if tab_title.startswith("Find Targets"):
|
| 359 |
+
existing_tabs.append(tab_title)
|
| 360 |
+
# Extract number if it exists
|
| 361 |
+
if tab_title != "Find Targets":
|
| 362 |
+
try:
|
| 363 |
+
num = int(tab_title.split()[-1])
|
| 364 |
+
tab_numbers.append(num)
|
| 365 |
+
except ValueError:
|
| 366 |
+
continue
|
| 367 |
+
|
| 368 |
+
# Determine new tab number
|
| 369 |
+
new_tab_number = 1
|
| 370 |
+
if tab_numbers:
|
| 371 |
+
new_tab_number = max(tab_numbers) + 1
|
| 372 |
|
| 373 |
loading_dialog.set_progress(40)
|
| 374 |
|
| 375 |
# Create new find targets controller and load data
|
| 376 |
+
find_targets_controller = self.settings.get_find_targets_window()
|
| 377 |
input_data = self.view.get_find_targets_input()
|
| 378 |
loading_dialog.set_progress(60)
|
| 379 |
|
| 380 |
find_targets_controller.find_targets(input_data)
|
| 381 |
loading_dialog.set_progress(80)
|
| 382 |
|
| 383 |
+
# Open new Find Targets tab with number if not the first one
|
| 384 |
+
tab_title = "Find Targets" if not existing_tabs else f"Find Targets {new_tab_number}"
|
| 385 |
+
self.settings.main_window.open_new_tab(tab_title, find_targets_controller)
|
| 386 |
loading_dialog.set_progress(100)
|
| 387 |
|
| 388 |
finally:
|
| 389 |
loading_dialog.close()
|
| 390 |
|
| 391 |
except Exception as e:
|
| 392 |
+
show_error(self.settings, "Error in open_find_targets() in Home", str(e))
|
| 393 |
|
| 394 |
+
def open_new_genome(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
try:
|
| 396 |
+
main_window = self.settings.main_window
|
| 397 |
existing_tab = main_window.find_tab_by_title("New Genome")
|
| 398 |
|
| 399 |
if existing_tab:
|
| 400 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 401 |
main_window._resize_for_tab("New Genome")
|
| 402 |
else:
|
| 403 |
+
new_genome_controller = self.settings.get_new_genome_window()
|
| 404 |
main_window.open_new_tab("New Genome", new_genome_controller)
|
| 405 |
except Exception as e:
|
| 406 |
+
show_error(self.settings, "Error in open_new_genome() in Home", str(e))
|
| 407 |
|
| 408 |
+
def open_new_endonuclease(self):
|
| 409 |
try:
|
| 410 |
+
# Create new endonuclease controller
|
| 411 |
+
new_endonuclease_controller = self.settings.get_new_endonuclease_window()
|
| 412 |
+
|
| 413 |
+
# Get the window from the controller
|
| 414 |
+
window = new_endonuclease_controller.view
|
| 415 |
+
|
| 416 |
+
# Set window properties
|
| 417 |
+
window.setWindowModality(Qt.WindowModality.ApplicationModal) # Make it modal
|
| 418 |
+
window.setMinimumSize(QSize(500, 650)) # Smaller minimum size
|
| 419 |
+
window.resize(QSize(600, 650)) # Set initial size
|
| 420 |
+
|
| 421 |
+
# Get the screen where the main window is
|
| 422 |
+
main_window = self.settings.main_window.view
|
| 423 |
+
screen = main_window.screen()
|
| 424 |
+
if not screen:
|
| 425 |
+
screen = QtWidgets.QApplication.primaryScreen()
|
| 426 |
+
|
| 427 |
+
# Get the available geometry of the screen (accounts for taskbars/docks)
|
| 428 |
+
screen_geometry = screen.availableGeometry()
|
| 429 |
+
|
| 430 |
+
# Calculate the center point of the screen
|
| 431 |
+
center_point = screen_geometry.center()
|
| 432 |
+
|
| 433 |
+
# Center the window on screen
|
| 434 |
+
window_geometry = window.frameGeometry()
|
| 435 |
+
window_geometry.moveCenter(center_point)
|
| 436 |
+
window.move(window_geometry.topLeft())
|
| 437 |
+
|
| 438 |
+
# Show the window
|
| 439 |
+
window.show()
|
| 440 |
+
window.raise_()
|
| 441 |
+
window.activateWindow()
|
| 442 |
+
|
| 443 |
+
# Store reference to prevent garbage collection
|
| 444 |
+
self._current_new_endonuclease_window = new_endonuclease_controller
|
| 445 |
+
|
| 446 |
+
self.logger.debug("New Endonuclease window opened successfully")
|
| 447 |
except Exception as e:
|
| 448 |
+
show_error(self.settings, "Error opening new endonuclease window", str(e))
|
| 449 |
|
| 450 |
+
def open_multitargeting_analysis(self):
|
| 451 |
try:
|
| 452 |
start_time = time.time()
|
| 453 |
self.logger.debug("Starting multitargeting analysis module launch")
|
| 454 |
|
| 455 |
+
main_window = self.settings.main_window
|
| 456 |
existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
|
| 457 |
|
| 458 |
tab_check_time = time.time()
|
|
|
|
| 464 |
self.logger.debug(f"Switched to existing tab: {time.time() - tab_check_time:.2f} seconds")
|
| 465 |
else:
|
| 466 |
controller_start = time.time()
|
| 467 |
+
multitargeting_controller = self.settings.get_multitargeting_window()
|
| 468 |
self.logger.debug(f"Controller creation took: {time.time() - controller_start:.2f} seconds")
|
| 469 |
|
| 470 |
tab_open_start = time.time()
|
|
|
|
| 473 |
|
| 474 |
self.logger.debug(f"Total multitargeting module launch took: {time.time() - start_time:.2f} seconds")
|
| 475 |
except Exception as e:
|
| 476 |
+
show_error(self.settings, "Error in open_multitargeting_analysis() in Home", str(e))
|
| 477 |
|
| 478 |
+
def open_population_analysis(self):
|
| 479 |
try:
|
| 480 |
+
main_window = self.settings.main_window
|
| 481 |
existing_tab = main_window.find_tab_by_title("Population Analysis")
|
| 482 |
if existing_tab:
|
| 483 |
main_window.view.tab_widget.setCurrentWidget(existing_tab)
|
| 484 |
main_window._resize_for_tab("Population Analysis")
|
| 485 |
else:
|
| 486 |
+
population_analysis_controller = self.settings.get_population_analysis_window()
|
| 487 |
main_window.open_new_tab("Population Analysis", population_analysis_controller)
|
| 488 |
except Exception as e:
|
| 489 |
+
show_error(self.settings, "Error in open_population_analysis() in Home", str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
+
def open_ncbi(self):
|
| 492 |
try:
|
| 493 |
+
ncbi_controller = self.settings.get_ncbi_window()
|
| 494 |
+
self.settings.main_window.open_new_tab("NCBI Download Tool", ncbi_controller)
|
| 495 |
except Exception as e:
|
| 496 |
+
show_error(self.settings, "Error in open_ncbi() in main", str(e))
|
| 497 |
|
| 498 |
+
# Event Handlers
|
| 499 |
+
def gather_settings(self):
|
| 500 |
+
"""Process input data and direct to appropriate view"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
try:
|
| 502 |
+
input_data = self.view.get_find_targets_input()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
|
| 504 |
+
if input_data['search_type'] == 'sequence':
|
| 505 |
+
sequence = input_data['search_query'].strip()
|
| 506 |
+
if len(sequence) < 100:
|
| 507 |
+
QMessageBox.warning(
|
| 508 |
+
self.view,
|
| 509 |
+
"Sequence Too Short",
|
| 510 |
+
"The sequence given is too small. At least 100 characters are required."
|
| 511 |
+
)
|
| 512 |
+
return
|
| 513 |
+
if len(sequence) > 10000:
|
| 514 |
+
QMessageBox.warning(
|
| 515 |
+
self.view,
|
| 516 |
+
"Sequence Too Long",
|
| 517 |
+
"The sequence given is too large. Maximum allowed length is 10,000 base pairs."
|
| 518 |
+
)
|
| 519 |
+
return
|
| 520 |
+
self.open_view_targets(input_data)
|
| 521 |
+
elif input_data['search_type'] == 'position':
|
| 522 |
+
self.open_view_targets(input_data)
|
| 523 |
+
else:
|
| 524 |
+
self.open_find_targets()
|
| 525 |
except Exception as e:
|
| 526 |
+
show_error(self.settings, "Error in gather_settings", str(e))
|
| 527 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
|
|
@@ -1,262 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from PyQt6 import QtWidgets, QtCore
|
| 3 |
-
from PyQt6.QtWidgets import QMainWindow
|
| 4 |
-
from views.MainWindowView import MainWindowView
|
| 5 |
-
from models.MainWindowModel import MainWindowModel
|
| 6 |
-
from controllers.MultitargetingWindowController import MultitargetingWindowController
|
| 7 |
-
from utils.ui import show_error, show_message, scale_ui, center_ui, position_window
|
| 8 |
-
from utils.web import ncbi_page, repo_page, ncbi_blast_page
|
| 9 |
-
from PyQt6.QtCore import QObject
|
| 10 |
-
|
| 11 |
-
class MainWindowController(QObject):
|
| 12 |
-
def __init__(self, global_settings):
|
| 13 |
-
super().__init__()
|
| 14 |
-
self.global_settings = global_settings
|
| 15 |
-
self.logger = global_settings.get_logger()
|
| 16 |
-
try:
|
| 17 |
-
self.model = MainWindowModel(global_settings)
|
| 18 |
-
self.view = MainWindowView(global_settings)
|
| 19 |
-
# self.setup_connections()
|
| 20 |
-
# self.init_ui()
|
| 21 |
-
self.show()
|
| 22 |
-
except Exception as e:
|
| 23 |
-
show_error(global_settings, "Error initializing MainWindowController", str(e))
|
| 24 |
-
raise
|
| 25 |
-
|
| 26 |
-
def setup_connections(self):
|
| 27 |
-
try:
|
| 28 |
-
# menuBar
|
| 29 |
-
self.view.action_change_directory.triggered.connect(self.change_database_directory)
|
| 30 |
-
# self.view.action_exit.triggered.connect(self.close)
|
| 31 |
-
# self.view.action_open_genome_browser.triggered.connect(self.open_genome_browser)
|
| 32 |
-
self.view.action_open_repository.triggered.connect(self.open_repository_website)
|
| 33 |
-
self.view.action_open_NCBI_BLAST.triggered.connect(self.open_ncbi_blast_website)
|
| 34 |
-
self.view.action_open_NCBI.triggered.connect(self.open_ncbi_website)
|
| 35 |
-
|
| 36 |
-
# grpNavigationMenu
|
| 37 |
-
self.view.push_button_new_genome.clicked.connect(self.open_new_genome_widget)
|
| 38 |
-
self.view.push_button_new_endonuclease.clicked.connect(self.open_new_endonuclease_widget)
|
| 39 |
-
self.view.push_button_multitargeting_analysis.clicked.connect(self.open_multitargeting_analysis_widget)
|
| 40 |
-
self.view.push_button_population_analysis.clicked.connect(self.open_population_analysis_widget)
|
| 41 |
-
|
| 42 |
-
# grpStep1
|
| 43 |
-
self.view.combo_box_organism.currentIndexChanged.connect(self.update_combo_box_endonuclease)
|
| 44 |
-
|
| 45 |
-
# grpStep2
|
| 46 |
-
self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi_window)
|
| 47 |
-
|
| 48 |
-
# grpStep3
|
| 49 |
-
self.view.radio_button_feature.clicked.connect(self.toggle_annotation)
|
| 50 |
-
self.view.radio_button_position.clicked.connect(self.toggle_annotation)
|
| 51 |
-
self.view.push_button_find_targets.clicked.connect(self.gather_settings)
|
| 52 |
-
self.view.push_button_view_targets.clicked.connect(self.view_results)
|
| 53 |
-
self.view.push_button_generate_library.clicked.connect(self.prep_gen_lib)
|
| 54 |
-
# self.view.theme_toggle_button.clicked.connect(self.toggle_theme)
|
| 55 |
-
|
| 56 |
-
# Custom title bar connections
|
| 57 |
-
self.view.theme_toggle_button.clicked.connect(self.toggle_theme)
|
| 58 |
-
self.view.minimize_button.clicked.connect(self.view.showMinimized)
|
| 59 |
-
self.view.maximize_button.clicked.connect(self.toggle_maximize)
|
| 60 |
-
self.view.close_button.clicked.connect(self.view.close)
|
| 61 |
-
except Exception as e:
|
| 62 |
-
show_error(self.global_settings, "Error setting up connections in MainWindowController", str(e))
|
| 63 |
-
|
| 64 |
-
def init_ui(self):
|
| 65 |
-
try:
|
| 66 |
-
self.view.push_button_view_targets.setEnabled(False)
|
| 67 |
-
self.view.push_button_generate_library.setEnabled(False)
|
| 68 |
-
self.load_combo_box_data()
|
| 69 |
-
self.view.reset_progress_bar()
|
| 70 |
-
except Exception as e:
|
| 71 |
-
show_error(self.global_settings, "Error initializing UI in MainWindowController", str(e))
|
| 72 |
-
|
| 73 |
-
def load_combo_box_data(self):
|
| 74 |
-
try:
|
| 75 |
-
self.model.load_data()
|
| 76 |
-
self.update_combo_box_data()
|
| 77 |
-
except Exception as e:
|
| 78 |
-
show_error(self.global_settings, "Error loading dropdown data in MainWindowController", str(e))
|
| 79 |
-
|
| 80 |
-
def update_combo_box_data(self):
|
| 81 |
-
organism_to_endonuclease = self.model.get_organism_to_endonuclease()
|
| 82 |
-
annotation_files = self.model.get_annotation_files()
|
| 83 |
-
|
| 84 |
-
self.logger.debug(f"Updating Organisms combo box with organisms: {organism_to_endonuclease.keys()} in Main window")
|
| 85 |
-
self.view.update_combo_box_organism(list(organism_to_endonuclease.keys()))
|
| 86 |
-
|
| 87 |
-
self.update_combo_box_endonuclease()
|
| 88 |
-
|
| 89 |
-
self.logger.debug(f"Updating Annotation files combo box with annotation files: {annotation_files} in Main window")
|
| 90 |
-
self.view.update_combo_box_annotation_files(annotation_files)
|
| 91 |
-
|
| 92 |
-
def update_combo_box_endonuclease(self):
|
| 93 |
-
selected_organism = self.view.combo_box_organism.currentText()
|
| 94 |
-
endonuclease = self.model.get_organism_to_endonuclease().get(selected_organism, [])
|
| 95 |
-
self.logger.debug(f"Updating endonuclease combo box for organism {selected_organism} with endonuclease: {endonuclease} in Main window")
|
| 96 |
-
self.view.update_combo_box_endonuclease(endonuclease)
|
| 97 |
-
|
| 98 |
-
# Event Handlers
|
| 99 |
-
def gather_settings(self):
|
| 100 |
-
# Implementation for gathering settings
|
| 101 |
-
pass
|
| 102 |
-
|
| 103 |
-
def view_results(self):
|
| 104 |
-
# Implementation for viewing results
|
| 105 |
-
pass
|
| 106 |
-
|
| 107 |
-
def toggle_annotation(self):
|
| 108 |
-
# Implementation for toggling annotation
|
| 109 |
-
pass
|
| 110 |
-
|
| 111 |
-
def prep_gen_lib(self):
|
| 112 |
-
# Implementation for preparing gene library
|
| 113 |
-
pass
|
| 114 |
-
|
| 115 |
-
def open_new_genome_widget(self):
|
| 116 |
-
try:
|
| 117 |
-
new_genome_window = self.global_settings.new_genome_window
|
| 118 |
-
self._open_widget("New Genome", new_genome_window.view)
|
| 119 |
-
except Exception as e:
|
| 120 |
-
show_error(self.global_settings, "Error in open_new_genome_widget() in main", e)
|
| 121 |
-
|
| 122 |
-
def open_new_endonuclease_widget(self):
|
| 123 |
-
try:
|
| 124 |
-
new_endo_window = self.global_settings.new_endonuclease_window
|
| 125 |
-
self._open_widget("New Endonuclease", new_endo_window.view)
|
| 126 |
-
except Exception as e:
|
| 127 |
-
show_error(self.global_settings, "Error in open_new_endonuclease_widget() in main", str(e))
|
| 128 |
-
|
| 129 |
-
def open_multitargeting_analysis_widget(self):
|
| 130 |
-
try:
|
| 131 |
-
multitargeting_window = self.global_settings.multitargeting_window
|
| 132 |
-
self._open_widget("Multitargeting Analysis", multitargeting_window)
|
| 133 |
-
except Exception as e:
|
| 134 |
-
show_error(self.global_settings, "Error in open_multitargeting_analysis_widget() in main", str(e))
|
| 135 |
-
|
| 136 |
-
def open_population_analysis_widget(self):
|
| 137 |
-
try:
|
| 138 |
-
population_analysis_window = self.global_settings.population_analysis_window
|
| 139 |
-
self._open_widget("Population Analysis", population_analysis_window)
|
| 140 |
-
except Exception as e:
|
| 141 |
-
show_error(self.global_settings, "Error in open_population_analysis_widget() in main", str(e))
|
| 142 |
-
|
| 143 |
-
def launch_populate_fna_files(self):
|
| 144 |
-
# Implementation for launching populate FNA files
|
| 145 |
-
pass
|
| 146 |
-
|
| 147 |
-
def open_ncbi_window(self):
|
| 148 |
-
try:
|
| 149 |
-
ncbi_window = self.global_settings.ncbi_window
|
| 150 |
-
self._open_widget("NCBI Window", ncbi_window.view)
|
| 151 |
-
except Exception as e:
|
| 152 |
-
show_error(self.global_settings, "Error in open_ncbi_window() in main", str(e))
|
| 153 |
-
|
| 154 |
-
def toggle_theme(self):
|
| 155 |
-
try:
|
| 156 |
-
self.global_settings.toggle_dark_mode()
|
| 157 |
-
self.view.update_theme_icon()
|
| 158 |
-
self.apply_theme()
|
| 159 |
-
except Exception as e:
|
| 160 |
-
self.logger.error(f"Error toggling theme: {str(e)}", exc_info=True)
|
| 161 |
-
show_error(self.global_settings, "Error toggling theme", str(e))
|
| 162 |
-
|
| 163 |
-
def toggle_maximize(self):
|
| 164 |
-
if self.view.isMaximized():
|
| 165 |
-
self.view.showNormal()
|
| 166 |
-
else:
|
| 167 |
-
self.view.showMaximized()
|
| 168 |
-
|
| 169 |
-
def apply_theme(self):
|
| 170 |
-
# Apply theme to all widgets
|
| 171 |
-
if self.global_settings.is_dark_mode():
|
| 172 |
-
# Apply dark theme
|
| 173 |
-
self.view.setStyleSheet("""
|
| 174 |
-
QWidget { background-color: #2b2b2b; color: #ffffff; }
|
| 175 |
-
QPushButton { background-color: #3a3a3a; border: 1px solid #5a5a5a; }
|
| 176 |
-
QPushButton:hover { background-color: #4a4a4a; }
|
| 177 |
-
QLineEdit, QTextEdit, QPlainTextEdit { background-color: #3a3a3a; border: 1px solid #5a5a5a; }
|
| 178 |
-
QComboBox { background-color: #3a3a3a; border: 1px solid #5a5a5a; }
|
| 179 |
-
QMenuBar { background-color: #2b2b2b; }
|
| 180 |
-
QMenuBar::item:selected { background-color: #3a3a3a; }
|
| 181 |
-
QMenu { background-color: #2b2b2b; }
|
| 182 |
-
QMenu::item:selected { background-color: #3a3a3a; }
|
| 183 |
-
""")
|
| 184 |
-
else:
|
| 185 |
-
# Apply light theme
|
| 186 |
-
self.view.setStyleSheet("""
|
| 187 |
-
QWidget { background-color: #f0f0f0; color: #000000; }
|
| 188 |
-
QPushButton { background-color: #e0e0e0; border: 1px solid #c0c0c0; }
|
| 189 |
-
QPushButton:hover { background-color: #d0d0d0; }
|
| 190 |
-
QLineEdit, QTextEdit, QPlainTextEdit { background-color: #ffffff; border: 1px solid #c0c0c0; }
|
| 191 |
-
QComboBox { background-color: #ffffff; border: 1px solid #c0c0c0; }
|
| 192 |
-
QMenuBar { background-color: #f0f0f0; }
|
| 193 |
-
QMenuBar::item:selected { background-color: #e0e0e0; }
|
| 194 |
-
QMenu { background-color: #f0f0f0; }
|
| 195 |
-
QMenu::item:selected { background-color: #e0e0e0; }
|
| 196 |
-
""")
|
| 197 |
-
|
| 198 |
-
# Utility Functions
|
| 199 |
-
def some_long_running_task(self):
|
| 200 |
-
if self.view.progress_bar:
|
| 201 |
-
self.view.reset_progress()
|
| 202 |
-
# ... (perform task steps)
|
| 203 |
-
if self.view.progress_bar:
|
| 204 |
-
self.view.set_progress(50)
|
| 205 |
-
# ... (more task steps)
|
| 206 |
-
if self.view.progress_bar:
|
| 207 |
-
self.view.set_progress(100)
|
| 208 |
-
|
| 209 |
-
def show(self):
|
| 210 |
-
try:
|
| 211 |
-
saved_position = self.global_settings.load_window_position("main_window")
|
| 212 |
-
if saved_position:
|
| 213 |
-
self.view.move(saved_position)
|
| 214 |
-
else:
|
| 215 |
-
center_ui(self.view)
|
| 216 |
-
self.view.show()
|
| 217 |
-
self.apply_theme()
|
| 218 |
-
except Exception as e:
|
| 219 |
-
self.global_settings.logger.error(f"Error showing main window: {str(e)}", exc_info=True)
|
| 220 |
-
show_error(self.global_settings, "Error showing main window", e)
|
| 221 |
-
|
| 222 |
-
def closeEvent(self, event):
|
| 223 |
-
self.global_settings.save_window_position("main_window", self.view.pos())
|
| 224 |
-
event.accept()
|
| 225 |
-
|
| 226 |
-
def change_database_directory(self):
|
| 227 |
-
try:
|
| 228 |
-
new_directory = QtWidgets.QFileDialog.getExistingDirectory(
|
| 229 |
-
self.view, "Select Database Directory", self.global_settings.CSPR_DB,
|
| 230 |
-
QtWidgets.QFileDialog.Option.ShowDirsOnly
|
| 231 |
-
)
|
| 232 |
-
if new_directory:
|
| 233 |
-
if self.global_settings.validate_db_path(new_directory):
|
| 234 |
-
self.global_settings.save_database_path(new_directory)
|
| 235 |
-
self.global_settings.initialize_app_directories()
|
| 236 |
-
self.load_combo_box_data()
|
| 237 |
-
show_message(12, QtWidgets.QMessageBox.Icon.Information,
|
| 238 |
-
"Success", "Database directory changed successfully.")
|
| 239 |
-
else:
|
| 240 |
-
show_message(12, QtWidgets.QMessageBox.Icon.Warning,
|
| 241 |
-
"Invalid Directory", "The selected directory does not contain valid CSPR files.")
|
| 242 |
-
except Exception as e:
|
| 243 |
-
self.logger.error(f"Error changing database directory: {str(e)}", exc_info=True)
|
| 244 |
-
show_error(self.global_settings, "Error changing database directory", str(e))
|
| 245 |
-
|
| 246 |
-
def open_ncbi_website(self):
|
| 247 |
-
ncbi_page()
|
| 248 |
-
|
| 249 |
-
def open_repository_website(self):
|
| 250 |
-
repo_page()
|
| 251 |
-
|
| 252 |
-
def open_ncbi_blast_website(self):
|
| 253 |
-
ncbi_blast_page()
|
| 254 |
-
|
| 255 |
-
def _open_widget(self, title, widget) -> None:
|
| 256 |
-
index = self.view.stacked_widget.indexOf(widget)
|
| 257 |
-
if index == -1:
|
| 258 |
-
self.view.stacked_widget.addWidget(widget)
|
| 259 |
-
index = self.view.stacked_widget.indexOf(widget)
|
| 260 |
-
self.view.stacked_widget.setCurrentIndex(index)
|
| 261 |
-
# Make sure the stacked widget is visible
|
| 262 |
-
self.view.stacked_widget.show()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -22,12 +22,28 @@ class MainWindowController(LoggingMixin):
|
|
| 22 |
self.startup_size = QSize(750, 550)
|
| 23 |
self.current_tab = None
|
| 24 |
self.previous_size = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
try:
|
| 27 |
self.view = MainWindowView(self.settings)
|
| 28 |
self._setup_connections()
|
| 29 |
self._init_ui()
|
| 30 |
self.settings.check_and_emit_first_time_startup()
|
|
|
|
|
|
|
| 31 |
except Exception as e:
|
| 32 |
self.log_error("__init__", e)
|
| 33 |
show_error(self.settings, "Error initializing MainWindowController", str(e))
|
|
@@ -43,21 +59,25 @@ class MainWindowController(LoggingMixin):
|
|
| 43 |
|
| 44 |
# Tab bar
|
| 45 |
self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
|
| 46 |
-
self.view.tab_widget.
|
| 47 |
self.view.tab_widget.currentChanged.connect(self._on_current_tab_changed)
|
| 48 |
|
| 49 |
self.settings.first_time_startup.connect(self._handle_first_time_startup)
|
| 50 |
|
| 51 |
# Add Button Menu
|
| 52 |
-
|
| 53 |
-
# self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_tab)
|
| 54 |
|
| 55 |
# Settings Menu
|
| 56 |
self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
def _init_ui(self):
|
| 59 |
self.log_method_call("_init_ui")
|
| 60 |
|
|
|
|
| 61 |
if self.is_first_time_startup:
|
| 62 |
self.log_info("First time startup detected. Opening startup tab.")
|
| 63 |
self._open_startup_tab()
|
|
@@ -66,21 +86,46 @@ class MainWindowController(LoggingMixin):
|
|
| 66 |
db_path = self.settings.get_db_path()
|
| 67 |
is_valid, message = self.settings.validate_db_path(db_path)
|
| 68 |
|
| 69 |
-
if
|
| 70 |
-
self.log_info(f"Database path is valid: {db_path}")
|
| 71 |
-
self._open_home_tab()
|
| 72 |
-
else:
|
| 73 |
self.log_warning(f"Invalid database path: {db_path}. {message}")
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
def _handle_first_time_startup(self):
|
| 77 |
self.log_info("First time startup signal received")
|
| 78 |
self.is_first_time_startup = True
|
| 79 |
self._open_startup_tab()
|
| 80 |
|
| 81 |
-
def _open_startup_tab(self):
|
| 82 |
try:
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
self.open_new_tab("Startup", self.startup_controller)
|
| 85 |
except Exception as e:
|
| 86 |
self.log_error("_open_startup_tab", e)
|
|
@@ -109,12 +154,29 @@ class MainWindowController(LoggingMixin):
|
|
| 109 |
self._center_window()
|
| 110 |
|
| 111 |
def _center_window(self):
|
|
|
|
| 112 |
try:
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
frame_geometry = self.view.frameGeometry()
|
|
|
|
|
|
|
| 115 |
frame_geometry.moveCenter(center_point)
|
|
|
|
|
|
|
| 116 |
self.view.move(frame_geometry.topLeft())
|
| 117 |
-
|
|
|
|
| 118 |
except Exception as e:
|
| 119 |
self.log_error("_center_window", e)
|
| 120 |
show_error(self.settings, "Error centering window", str(e))
|
|
@@ -141,22 +203,87 @@ class MainWindowController(LoggingMixin):
|
|
| 141 |
show_error(self.settings, "Error changing database directory", str(e))
|
| 142 |
|
| 143 |
def _handle_invalid_directory(self, new_directory, message):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
reply = QtWidgets.QMessageBox.question(
|
| 145 |
self.view,
|
| 146 |
"Invalid Directory",
|
| 147 |
-
|
| 148 |
-
"Would you like to analyze a new genome in this directory?",
|
| 149 |
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
| 150 |
QtWidgets.QMessageBox.StandardButton.No
|
| 151 |
)
|
| 152 |
|
|
|
|
|
|
|
| 153 |
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
| 154 |
-
self.
|
| 155 |
-
|
| 156 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
else:
|
|
|
|
| 158 |
show_message("Operation Cancelled", "Database directory change cancelled.")
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
def _process_valid_directory(self, new_directory):
|
| 161 |
try:
|
| 162 |
self.settings.save_db_path(new_directory)
|
|
@@ -215,7 +342,7 @@ class MainWindowController(LoggingMixin):
|
|
| 215 |
if existing_tab:
|
| 216 |
self.log_debug(f"Tab '{title}' already exists, switching to it")
|
| 217 |
self.view.tab_widget.setCurrentWidget(existing_tab)
|
| 218 |
-
self._resize_for_tab(title)
|
| 219 |
return
|
| 220 |
|
| 221 |
# Create widget from content
|
|
@@ -237,87 +364,85 @@ class MainWindowController(LoggingMixin):
|
|
| 237 |
self.view.tab_widget.setCurrentIndex(index)
|
| 238 |
self.tab_widgets['widgets'][title] = wrapper
|
| 239 |
|
| 240 |
-
self._resize_for_tab(title)
|
| 241 |
self.log_info(f"Tab '{title}' opened successfully at index {index}")
|
| 242 |
|
| 243 |
except Exception as e:
|
| 244 |
self.log_error("open_new_tab", e)
|
| 245 |
show_error(self.settings, f"Error opening tab '{title}'", str(e))
|
| 246 |
|
| 247 |
-
def _resize_for_tab(self, title):
|
|
|
|
| 248 |
try:
|
|
|
|
|
|
|
|
|
|
| 249 |
if title == "Startup":
|
| 250 |
-
#
|
| 251 |
-
self.view.setFixedSize(
|
| 252 |
-
|
| 253 |
-
# Store current size before applying constraints
|
| 254 |
-
|
|
|
|
| 255 |
self.previous_size = self.view.size()
|
| 256 |
|
| 257 |
-
# Set minimum dimensions for these tabs
|
| 258 |
-
min_width = 1300
|
| 259 |
-
min_height = 800
|
| 260 |
-
|
| 261 |
# Calculate new dimensions
|
| 262 |
-
new_width = max(self.view.width(),
|
| 263 |
-
new_height = max(self.view.height(),
|
| 264 |
|
| 265 |
# Only resize if dimensions need to increase
|
| 266 |
if new_width > self.view.width() or new_height > self.view.height():
|
| 267 |
self.view.resize(QSize(new_width, new_height))
|
| 268 |
|
| 269 |
-
# Set
|
| 270 |
-
self.view.setMinimumSize(
|
| 271 |
-
self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
| 272 |
-
else:
|
| 273 |
-
# For all other tabs
|
| 274 |
-
self.view.setMinimumSize(QSize(400, 300))
|
| 275 |
self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
| 276 |
|
| 277 |
-
# Restore previous size if available and coming from
|
| 278 |
-
if self.current_tab
|
| 279 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
|
| 281 |
self.view.resize(self.shared_tab_size)
|
| 282 |
|
| 283 |
# Update the current tab
|
| 284 |
self.current_tab = title
|
| 285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
except Exception as e:
|
| 287 |
self.log_error("_resize_for_tab", e)
|
| 288 |
|
| 289 |
def _close_tab(self, index):
|
| 290 |
"""Handle tab closure using CloseableTabWidget"""
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
# Handle post-close operations
|
| 310 |
-
if title == "New Genome":
|
| 311 |
-
home_tab = self.find_tab_by_title("Home")
|
| 312 |
-
if home_tab:
|
| 313 |
-
home_controller = self.settings.get_home_window()
|
| 314 |
-
home_controller.refresh_data()
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
new_tab_title = self.view.tab_widget.tabText(new_index)
|
| 320 |
-
self._resize_for_tab(new_tab_title)
|
| 321 |
|
| 322 |
def _toggle_theme(self):
|
| 323 |
try:
|
|
@@ -332,11 +457,11 @@ class MainWindowController(LoggingMixin):
|
|
| 332 |
saved_position = self.settings.load_window_position("main_window")
|
| 333 |
if saved_position:
|
| 334 |
self.view.move(saved_position)
|
| 335 |
-
else:
|
| 336 |
-
# center_ui(self.view)
|
| 337 |
-
pass
|
| 338 |
self.view.show()
|
| 339 |
self.view.apply_theme()
|
|
|
|
|
|
|
|
|
|
| 340 |
except Exception as e:
|
| 341 |
self.log_error("show", e)
|
| 342 |
show_error(self.settings, "Error showing main window", e)
|
|
@@ -410,20 +535,101 @@ class MainWindowController(LoggingMixin):
|
|
| 410 |
if old_tab_title and old_tab_title != "Startup":
|
| 411 |
self.previous_size = self.view.size()
|
| 412 |
|
| 413 |
-
self._resize_for_tab(new_tab_title)
|
| 414 |
|
| 415 |
except Exception as e:
|
| 416 |
self.log_error("_on_current_tab_changed", e)
|
| 417 |
|
| 418 |
-
def
|
| 419 |
-
"""Opens the new endonuclease window"""
|
| 420 |
try:
|
|
|
|
| 421 |
new_endonuclease_controller = self.settings.get_new_endonuclease_window()
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
except Exception as e:
|
| 424 |
-
self.log_error("
|
| 425 |
show_error(self.settings, "Error opening new endonuclease window", str(e))
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
|
|
|
|
| 22 |
self.startup_size = QSize(750, 550)
|
| 23 |
self.current_tab = None
|
| 24 |
self.previous_size = None
|
| 25 |
+
# Add flag to track dialog state
|
| 26 |
+
self._cspr_deletion_dialog_shown = False
|
| 27 |
+
|
| 28 |
+
# Define minimum sizes for different tab types
|
| 29 |
+
self.tab_min_sizes = {
|
| 30 |
+
"Startup": QSize(750, 550), # Startup has a fixed size
|
| 31 |
+
"View Targets": QSize(1300, 800), # Large tabs
|
| 32 |
+
"Multitargeting Analysis": QSize(1300, 800),
|
| 33 |
+
"Population Analysis": QSize(1000, 700), # Medium-large tabs
|
| 34 |
+
"Find Targets": QSize(1000, 700),
|
| 35 |
+
"New Genome": QSize(800, 600), # Medium tabs
|
| 36 |
+
"Home": QSize(800, 600),
|
| 37 |
+
"default": QSize(600, 500) # Default minimum size for any other tab
|
| 38 |
+
}
|
| 39 |
|
| 40 |
try:
|
| 41 |
self.view = MainWindowView(self.settings)
|
| 42 |
self._setup_connections()
|
| 43 |
self._init_ui()
|
| 44 |
self.settings.check_and_emit_first_time_startup()
|
| 45 |
+
# Center the window after initialization
|
| 46 |
+
self._center_window()
|
| 47 |
except Exception as e:
|
| 48 |
self.log_error("__init__", e)
|
| 49 |
show_error(self.settings, "Error initializing MainWindowController", str(e))
|
|
|
|
| 59 |
|
| 60 |
# Tab bar
|
| 61 |
self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
|
| 62 |
+
self.view.tab_widget.tab_closing.connect(self._close_tab)
|
| 63 |
self.view.tab_widget.currentChanged.connect(self._on_current_tab_changed)
|
| 64 |
|
| 65 |
self.settings.first_time_startup.connect(self._handle_first_time_startup)
|
| 66 |
|
| 67 |
# Add Button Menu
|
| 68 |
+
self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_window)
|
|
|
|
| 69 |
|
| 70 |
# Settings Menu
|
| 71 |
self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
|
| 72 |
|
| 73 |
+
# Database state changes
|
| 74 |
+
self.settings.db_manager.db_state_changed.connect(self._on_db_state_changed)
|
| 75 |
+
self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
|
| 76 |
+
|
| 77 |
def _init_ui(self):
|
| 78 |
self.log_method_call("_init_ui")
|
| 79 |
|
| 80 |
+
# Check if it's first time startup
|
| 81 |
if self.is_first_time_startup:
|
| 82 |
self.log_info("First time startup detected. Opening startup tab.")
|
| 83 |
self._open_startup_tab()
|
|
|
|
| 86 |
db_path = self.settings.get_db_path()
|
| 87 |
is_valid, message = self.settings.validate_db_path(db_path)
|
| 88 |
|
| 89 |
+
if not is_valid:
|
|
|
|
|
|
|
|
|
|
| 90 |
self.log_warning(f"Invalid database path: {db_path}. {message}")
|
| 91 |
+
# Always open startup tab for invalid paths, keeping the existing path
|
| 92 |
+
self._open_startup_tab(keep_db_path=True)
|
| 93 |
+
return # Add explicit return to prevent further execution
|
| 94 |
+
|
| 95 |
+
self.log_info(f"Database path is valid: {db_path}")
|
| 96 |
+
self._open_home_tab()
|
| 97 |
|
| 98 |
def _handle_first_time_startup(self):
|
| 99 |
self.log_info("First time startup signal received")
|
| 100 |
self.is_first_time_startup = True
|
| 101 |
self._open_startup_tab()
|
| 102 |
|
| 103 |
+
def _open_startup_tab(self, keep_db_path=False):
|
| 104 |
try:
|
| 105 |
+
# First safely deactivate any existing controllers
|
| 106 |
+
for title, controller in list(self.tab_widgets['controllers'].items()):
|
| 107 |
+
try:
|
| 108 |
+
if hasattr(controller, 'deactivate'):
|
| 109 |
+
controller.deactivate()
|
| 110 |
+
except Exception as e:
|
| 111 |
+
self.logger.error(f"Error deactivating controller for {title}: {str(e)}")
|
| 112 |
+
|
| 113 |
+
# Force close all tabs
|
| 114 |
+
while self.view.tab_widget.count() > 0:
|
| 115 |
+
try:
|
| 116 |
+
widget = self.view.tab_widget.widget(0)
|
| 117 |
+
self.view.tab_widget.removeTab(0)
|
| 118 |
+
if widget:
|
| 119 |
+
widget.deleteLater()
|
| 120 |
+
except Exception as e:
|
| 121 |
+
self.logger.error(f"Error closing tab: {str(e)}")
|
| 122 |
+
|
| 123 |
+
# Clear tab tracking dictionaries
|
| 124 |
+
self.tab_widgets['widgets'].clear()
|
| 125 |
+
self.tab_widgets['controllers'].clear()
|
| 126 |
+
|
| 127 |
+
# Then open the startup tab
|
| 128 |
+
self.startup_controller = self.settings.get_startup_window(keep_db_path=keep_db_path)
|
| 129 |
self.open_new_tab("Startup", self.startup_controller)
|
| 130 |
except Exception as e:
|
| 131 |
self.log_error("_open_startup_tab", e)
|
|
|
|
| 154 |
self._center_window()
|
| 155 |
|
| 156 |
def _center_window(self):
|
| 157 |
+
"""Center the window on the current screen"""
|
| 158 |
try:
|
| 159 |
+
# Get the current screen where the window is or the primary screen
|
| 160 |
+
window_screen = self.view.screen()
|
| 161 |
+
if not window_screen:
|
| 162 |
+
window_screen = QtGui.QGuiApplication.primaryScreen()
|
| 163 |
+
|
| 164 |
+
# Get the geometry of the screen
|
| 165 |
+
screen_geometry = window_screen.availableGeometry()
|
| 166 |
+
|
| 167 |
+
# Calculate the center point
|
| 168 |
+
center_point = screen_geometry.center()
|
| 169 |
+
|
| 170 |
+
# Get the window geometry
|
| 171 |
frame_geometry = self.view.frameGeometry()
|
| 172 |
+
|
| 173 |
+
# Move the window's center to the screen's center
|
| 174 |
frame_geometry.moveCenter(center_point)
|
| 175 |
+
|
| 176 |
+
# Move the window to the calculated position
|
| 177 |
self.view.move(frame_geometry.topLeft())
|
| 178 |
+
|
| 179 |
+
self.log_debug(f"Window centered on screen at {self.view.pos()}")
|
| 180 |
except Exception as e:
|
| 181 |
self.log_error("_center_window", e)
|
| 182 |
show_error(self.settings, "Error centering window", str(e))
|
|
|
|
| 203 |
show_error(self.settings, "Error changing database directory", str(e))
|
| 204 |
|
| 205 |
def _handle_invalid_directory(self, new_directory, message):
|
| 206 |
+
"""Handle invalid directory selection with option to analyze new genomes"""
|
| 207 |
+
self.logger.debug("Entering _handle_invalid_directory") # Add entry log
|
| 208 |
+
|
| 209 |
+
# Always show error for non-existent directories
|
| 210 |
+
if message == "The selected directory does not exist.":
|
| 211 |
+
self.logger.debug("Directory does not exist, showing error") # Add debug log
|
| 212 |
+
show_error(self.settings, "Invalid Directory", message)
|
| 213 |
+
return
|
| 214 |
+
|
| 215 |
+
self.logger.debug("Showing analyze new genome dialog") # Add debug log
|
| 216 |
+
# Show dialog for analyzing new genome
|
| 217 |
reply = QtWidgets.QMessageBox.question(
|
| 218 |
self.view,
|
| 219 |
"Invalid Directory",
|
| 220 |
+
"Would you like to analyze a new genome in this directory? testing",
|
|
|
|
| 221 |
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
| 222 |
QtWidgets.QMessageBox.StandardButton.No
|
| 223 |
)
|
| 224 |
|
| 225 |
+
self.logger.debug(f"User reply to analyze new genome: {reply == QtWidgets.QMessageBox.StandardButton.Yes}") # Add debug log
|
| 226 |
+
|
| 227 |
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
| 228 |
+
self.logger.debug("User chose to analyze new genome") # Add debug log
|
| 229 |
+
# Temporarily disconnect validation signal to prevent warning
|
| 230 |
+
self.settings.db_manager.db_validation_changed.disconnect()
|
| 231 |
+
try:
|
| 232 |
+
# Set directory change flag
|
| 233 |
+
self.settings.db_manager.is_changing_directory = True
|
| 234 |
+
self.logger.debug(f"Starting directory change process to: {new_directory}")
|
| 235 |
+
|
| 236 |
+
# Save the new path without switching to startup tab
|
| 237 |
+
self.settings.save_db_path(new_directory)
|
| 238 |
+
self.settings.update_db_state()
|
| 239 |
+
|
| 240 |
+
# Store the new directory for later use
|
| 241 |
+
self._pending_directory_change = new_directory
|
| 242 |
+
|
| 243 |
+
# Open new genome tab without closing other tabs
|
| 244 |
+
self.logger.debug("Opening new genome tab") # Add debug log
|
| 245 |
+
self.open_new_genome_tab()
|
| 246 |
+
|
| 247 |
+
# Connect to the new genome tab's completion signal
|
| 248 |
+
new_genome_tab = self.find_tab_by_title("New Genome")
|
| 249 |
+
if new_genome_tab and new_genome_tab in self.tab_widgets['controllers']:
|
| 250 |
+
new_genome_controller = self.tab_widgets['controllers']["New Genome"]
|
| 251 |
+
new_genome_controller.process_completed.connect(self._on_new_genome_completed)
|
| 252 |
+
self.logger.debug("Connected to new genome completion signal") # Add debug log
|
| 253 |
+
|
| 254 |
+
except Exception as e:
|
| 255 |
+
self.logger.error(f"Error in _handle_invalid_directory: {str(e)}") # Add error log
|
| 256 |
+
raise
|
| 257 |
+
finally:
|
| 258 |
+
# Reconnect the signal after operation is complete
|
| 259 |
+
self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
|
| 260 |
+
self.logger.debug("Reconnected validation signal") # Add debug log
|
| 261 |
else:
|
| 262 |
+
self.logger.debug("User cancelled new genome analysis") # Add debug log
|
| 263 |
show_message("Operation Cancelled", "Database directory change cancelled.")
|
| 264 |
|
| 265 |
+
def _on_new_genome_completed(self):
|
| 266 |
+
"""Handle completion of new genome analysis"""
|
| 267 |
+
try:
|
| 268 |
+
if hasattr(self, '_pending_directory_change'):
|
| 269 |
+
# Show success message
|
| 270 |
+
show_message(
|
| 271 |
+
"Database Directory Change Complete",
|
| 272 |
+
f"Successfully changed database directory to:\n{self._pending_directory_change}",
|
| 273 |
+
QtWidgets.QMessageBox.Icon.Information
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Refresh home tab
|
| 277 |
+
home_tab = self.find_tab_by_title("Home")
|
| 278 |
+
if home_tab and home_tab in self.tab_widgets['controllers']:
|
| 279 |
+
home_controller = self.tab_widgets['controllers']["Home"]
|
| 280 |
+
home_controller.refresh_data()
|
| 281 |
+
|
| 282 |
+
# Clean up
|
| 283 |
+
delattr(self, '_pending_directory_change')
|
| 284 |
+
except Exception as e:
|
| 285 |
+
self.logger.error(f"Error handling new genome completion: {str(e)}")
|
| 286 |
+
|
| 287 |
def _process_valid_directory(self, new_directory):
|
| 288 |
try:
|
| 289 |
self.settings.save_db_path(new_directory)
|
|
|
|
| 342 |
if existing_tab:
|
| 343 |
self.log_debug(f"Tab '{title}' already exists, switching to it")
|
| 344 |
self.view.tab_widget.setCurrentWidget(existing_tab)
|
| 345 |
+
self._resize_for_tab(title, center_window=False) # Don't center when switching to existing tab
|
| 346 |
return
|
| 347 |
|
| 348 |
# Create widget from content
|
|
|
|
| 364 |
self.view.tab_widget.setCurrentIndex(index)
|
| 365 |
self.tab_widgets['widgets'][title] = wrapper
|
| 366 |
|
| 367 |
+
self._resize_for_tab(title, center_window=True) # Center when opening new tab
|
| 368 |
self.log_info(f"Tab '{title}' opened successfully at index {index}")
|
| 369 |
|
| 370 |
except Exception as e:
|
| 371 |
self.log_error("open_new_tab", e)
|
| 372 |
show_error(self.settings, f"Error opening tab '{title}'", str(e))
|
| 373 |
|
| 374 |
+
def _resize_for_tab(self, title, center_window=True):
|
| 375 |
+
"""Handle window resizing for different tab types"""
|
| 376 |
try:
|
| 377 |
+
# Get the minimum size for this tab type
|
| 378 |
+
min_size = self.tab_min_sizes.get(title, self.tab_min_sizes["default"])
|
| 379 |
+
|
| 380 |
if title == "Startup":
|
| 381 |
+
# Startup tab has a fixed size
|
| 382 |
+
self.view.setFixedSize(min_size)
|
| 383 |
+
else:
|
| 384 |
+
# Store current size before applying constraints if coming from a different size category
|
| 385 |
+
current_min_size = self.tab_min_sizes.get(self.current_tab, self.tab_min_sizes["default"])
|
| 386 |
+
if current_min_size != min_size:
|
| 387 |
self.previous_size = self.view.size()
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
# Calculate new dimensions
|
| 390 |
+
new_width = max(self.view.width(), min_size.width())
|
| 391 |
+
new_height = max(self.view.height(), min_size.height())
|
| 392 |
|
| 393 |
# Only resize if dimensions need to increase
|
| 394 |
if new_width > self.view.width() or new_height > self.view.height():
|
| 395 |
self.view.resize(QSize(new_width, new_height))
|
| 396 |
|
| 397 |
+
# Set size constraints
|
| 398 |
+
self.view.setMinimumSize(min_size)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
| 400 |
|
| 401 |
+
# Restore previous size if available and coming from a larger minimum size tab
|
| 402 |
+
if self.current_tab and self.previous_size:
|
| 403 |
+
current_min_size = self.tab_min_sizes.get(self.current_tab, self.tab_min_sizes["default"])
|
| 404 |
+
if current_min_size.width() > min_size.width() or current_min_size.height() > min_size.height():
|
| 405 |
+
# Only restore if the previous size is larger than our minimum
|
| 406 |
+
if (self.previous_size.width() >= min_size.width() and
|
| 407 |
+
self.previous_size.height() >= min_size.height()):
|
| 408 |
+
self.view.resize(self.previous_size)
|
| 409 |
elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
|
| 410 |
self.view.resize(self.shared_tab_size)
|
| 411 |
|
| 412 |
# Update the current tab
|
| 413 |
self.current_tab = title
|
| 414 |
|
| 415 |
+
# Center the window after resizing only if requested
|
| 416 |
+
if center_window:
|
| 417 |
+
self._center_window()
|
| 418 |
+
|
| 419 |
except Exception as e:
|
| 420 |
self.log_error("_resize_for_tab", e)
|
| 421 |
|
| 422 |
def _close_tab(self, index):
|
| 423 |
"""Handle tab closure using CloseableTabWidget"""
|
| 424 |
+
try:
|
| 425 |
+
if 0 <= index < self.view.tab_widget.count():
|
| 426 |
+
title = self.view.tab_widget.tabText(index)
|
| 427 |
+
|
| 428 |
+
# Safely deactivate controller if it exists
|
| 429 |
+
if title in self.tab_widgets['controllers']:
|
| 430 |
+
try:
|
| 431 |
+
controller = self.tab_widgets['controllers'][title]
|
| 432 |
+
if hasattr(controller, 'deactivate'):
|
| 433 |
+
controller.deactivate()
|
| 434 |
+
except Exception as e:
|
| 435 |
+
self.logger.error(f"Error deactivating controller for {title}: {str(e)}")
|
| 436 |
+
|
| 437 |
+
# Clean up references
|
| 438 |
+
if title in self.tab_widgets['widgets']:
|
| 439 |
+
del self.tab_widgets['widgets'][title]
|
| 440 |
+
if title in self.tab_widgets['controllers']:
|
| 441 |
+
del self.tab_widgets['controllers'][title]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
|
| 443 |
+
self.logger.debug(f"Closed tab '{title}' at index {index}")
|
| 444 |
+
except Exception as e:
|
| 445 |
+
self.logger.error(f"Error in _close_tab: {str(e)}")
|
|
|
|
|
|
|
| 446 |
|
| 447 |
def _toggle_theme(self):
|
| 448 |
try:
|
|
|
|
| 457 |
saved_position = self.settings.load_window_position("main_window")
|
| 458 |
if saved_position:
|
| 459 |
self.view.move(saved_position)
|
|
|
|
|
|
|
|
|
|
| 460 |
self.view.show()
|
| 461 |
self.view.apply_theme()
|
| 462 |
+
# Center the window after showing it
|
| 463 |
+
self._center_window()
|
| 464 |
+
print("Window initialized")
|
| 465 |
except Exception as e:
|
| 466 |
self.log_error("show", e)
|
| 467 |
show_error(self.settings, "Error showing main window", e)
|
|
|
|
| 535 |
if old_tab_title and old_tab_title != "Startup":
|
| 536 |
self.previous_size = self.view.size()
|
| 537 |
|
| 538 |
+
self._resize_for_tab(new_tab_title, center_window=False)
|
| 539 |
|
| 540 |
except Exception as e:
|
| 541 |
self.log_error("_on_current_tab_changed", e)
|
| 542 |
|
| 543 |
+
def open_new_endonuclease_window(self):
|
| 544 |
+
"""Opens the new endonuclease as a separate window"""
|
| 545 |
try:
|
| 546 |
+
# Create the controller
|
| 547 |
new_endonuclease_controller = self.settings.get_new_endonuclease_window()
|
| 548 |
+
|
| 549 |
+
# Get the window from the controller
|
| 550 |
+
window = new_endonuclease_controller.view
|
| 551 |
+
|
| 552 |
+
# Set window properties
|
| 553 |
+
window.setWindowModality(Qt.WindowModality.ApplicationModal) # Make it modal
|
| 554 |
+
window.setMinimumSize(QSize(800, 600)) # Set minimum size
|
| 555 |
+
|
| 556 |
+
# Center the window relative to the main window
|
| 557 |
+
main_window_center = self.view.geometry().center()
|
| 558 |
+
window_geometry = window.frameGeometry()
|
| 559 |
+
window_geometry.moveCenter(main_window_center)
|
| 560 |
+
window.move(window_geometry.topLeft())
|
| 561 |
+
|
| 562 |
+
# Show the window
|
| 563 |
+
window.show()
|
| 564 |
+
window.raise_()
|
| 565 |
+
window.activateWindow()
|
| 566 |
+
|
| 567 |
+
# Store reference to prevent garbage collection
|
| 568 |
+
self._current_new_endonuclease_window = new_endonuclease_controller
|
| 569 |
+
|
| 570 |
+
self.log_info("New Endonuclease window opened successfully")
|
| 571 |
except Exception as e:
|
| 572 |
+
self.log_error("open_new_endonuclease_window", e)
|
| 573 |
show_error(self.settings, "Error opening new endonuclease window", str(e))
|
| 574 |
|
| 575 |
+
def _on_db_validation_changed(self, is_valid, message):
|
| 576 |
+
"""Handle database validation state changes"""
|
| 577 |
+
if not is_valid:
|
| 578 |
+
self.logger.debug(f"Database validation failed: {message}")
|
| 579 |
+
|
| 580 |
+
# Only show dialog if we're not in startup tab
|
| 581 |
+
startup_tab = self.find_tab_by_title("Startup")
|
| 582 |
+
if startup_tab and self.view.tab_widget.currentWidget() == startup_tab:
|
| 583 |
+
return
|
| 584 |
+
|
| 585 |
+
# Switch to startup tab for invalid paths (including when files are deleted)
|
| 586 |
+
# but keep the current path
|
| 587 |
+
if not startup_tab:
|
| 588 |
+
self._open_startup_tab(keep_db_path=True)
|
| 589 |
+
return
|
| 590 |
+
|
| 591 |
+
else:
|
| 592 |
+
self._cspr_deletion_dialog_shown = False # Reset flag
|
| 593 |
+
if "Successfully changed database directory to:" in message:
|
| 594 |
+
show_message(
|
| 595 |
+
"Database Directory Change Complete",
|
| 596 |
+
message,
|
| 597 |
+
QtWidgets.QMessageBox.Icon.Information
|
| 598 |
+
)
|
| 599 |
+
# Refresh home tab if it exists
|
| 600 |
+
home_tab = self.find_tab_by_title("Home")
|
| 601 |
+
if home_tab and home_tab in self.tab_widgets['controllers']:
|
| 602 |
+
home_controller = self.tab_widgets['controllers']["Home"]
|
| 603 |
+
home_controller.refresh_data()
|
| 604 |
|
| 605 |
+
def _on_db_state_changed(self, is_valid, message, changes):
|
| 606 |
+
"""Handle database state changes"""
|
| 607 |
+
try:
|
| 608 |
+
self.logger.debug(f"Database state changed - Valid: {is_valid}, Message: {message}, Changes: {changes}")
|
| 609 |
+
|
| 610 |
+
# If path becomes invalid (including when files are deleted), switch to startup tab
|
| 611 |
+
if not is_valid:
|
| 612 |
+
# Skip only if we're already in startup tab
|
| 613 |
+
startup_tab = self.find_tab_by_title("Startup")
|
| 614 |
+
if startup_tab and self.view.tab_widget.currentWidget() == startup_tab:
|
| 615 |
+
return
|
| 616 |
+
|
| 617 |
+
# Skip only if we're in the process of changing directory for new genome
|
| 618 |
+
if self.settings.db_manager.is_changing_directory:
|
| 619 |
+
return
|
| 620 |
+
|
| 621 |
+
# Otherwise, switch to startup tab with current path
|
| 622 |
+
self._open_startup_tab(keep_db_path=True)
|
| 623 |
+
return
|
| 624 |
+
|
| 625 |
+
# Handle other state changes
|
| 626 |
+
if changes:
|
| 627 |
+
# Refresh home tab if it exists
|
| 628 |
+
home_tab = self.find_tab_by_title("Home")
|
| 629 |
+
if home_tab and home_tab in self.tab_widgets['controllers']:
|
| 630 |
+
home_controller = self.tab_widgets['controllers']["Home"]
|
| 631 |
+
home_controller.refresh_data()
|
| 632 |
+
|
| 633 |
+
except Exception as e:
|
| 634 |
+
self.logger.error(f"Error handling database state change: {str(e)}")
|
| 635 |
|
|
@@ -14,15 +14,28 @@ class NCBIWindowController:
|
|
| 14 |
def __init__(self, settings):
|
| 15 |
self.settings = settings
|
| 16 |
try:
|
|
|
|
| 17 |
self.logger = self.settings.get_logger()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
self.model = NCBIWindowModel(settings)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
self.view = NCBIWindowView(settings)
|
|
|
|
| 20 |
|
| 21 |
# Connect to the initialization complete signal
|
| 22 |
self.view.initialization_complete.connect(self.setup_connections)
|
| 23 |
|
| 24 |
self._init_ui()
|
|
|
|
|
|
|
| 25 |
except Exception as e:
|
|
|
|
| 26 |
show_error(self.settings, "Error initializing NCBIWindowController", str(e))
|
| 27 |
|
| 28 |
def setup_connections(self):
|
|
@@ -134,7 +147,7 @@ class NCBIWindowController:
|
|
| 134 |
self.logger.info(f"Processing ID: {id}")
|
| 135 |
|
| 136 |
urls = self.model.get_download_url(id, self.view.radio_button_collections_genbank.isChecked())
|
| 137 |
-
self.logger.info(f"Download URLs for ID {id}: {urls}")
|
| 138 |
|
| 139 |
if not urls:
|
| 140 |
self.logger.warning(f"No download URL found for ID: {id}")
|
|
|
|
| 14 |
def __init__(self, settings):
|
| 15 |
self.settings = settings
|
| 16 |
try:
|
| 17 |
+
start_time = time.time()
|
| 18 |
self.logger = self.settings.get_logger()
|
| 19 |
+
self.logger.debug("Starting NCBIWindowController initialization")
|
| 20 |
+
|
| 21 |
+
# Log model initialization time
|
| 22 |
+
model_start = time.time()
|
| 23 |
self.model = NCBIWindowModel(settings)
|
| 24 |
+
self.logger.debug(f"Model initialization took: {time.time() - model_start:.2f} seconds")
|
| 25 |
+
|
| 26 |
+
# Log view initialization time
|
| 27 |
+
view_start = time.time()
|
| 28 |
self.view = NCBIWindowView(settings)
|
| 29 |
+
self.logger.debug(f"View initialization took: {time.time() - view_start:.2f} seconds")
|
| 30 |
|
| 31 |
# Connect to the initialization complete signal
|
| 32 |
self.view.initialization_complete.connect(self.setup_connections)
|
| 33 |
|
| 34 |
self._init_ui()
|
| 35 |
+
|
| 36 |
+
self.logger.debug(f"Total NCBIWindowController initialization took: {time.time() - start_time:.2f} seconds")
|
| 37 |
except Exception as e:
|
| 38 |
+
self.logger.error(f"Error initializing NCBIWindowController: {str(e)}")
|
| 39 |
show_error(self.settings, "Error initializing NCBIWindowController", str(e))
|
| 40 |
|
| 41 |
def setup_connections(self):
|
|
|
|
| 147 |
self.logger.info(f"Processing ID: {id}")
|
| 148 |
|
| 149 |
urls = self.model.get_download_url(id, self.view.radio_button_collections_genbank.isChecked())
|
| 150 |
+
self.logger.info(f"Download URLs for ID {id}: {urls} in database")
|
| 151 |
|
| 152 |
if not urls:
|
| 153 |
self.logger.warning(f"No download URL found for ID: {id}")
|
|
@@ -3,11 +3,13 @@ from models.NewGenomeWindowModel import NewGenomeWindowModel
|
|
| 3 |
from views.NewGenomeWindowView import NewGenomeWindowView
|
| 4 |
from utils.ui import show_message, show_error
|
| 5 |
import os
|
|
|
|
| 6 |
|
| 7 |
class NewGenomeWindowController:
|
| 8 |
def __init__(self, global_settings):
|
| 9 |
self.settings = global_settings
|
| 10 |
self.logger = global_settings.get_logger()
|
|
|
|
| 11 |
|
| 12 |
try:
|
| 13 |
self.model = NewGenomeWindowModel(self.settings)
|
|
@@ -66,25 +68,37 @@ class NewGenomeWindowController:
|
|
| 66 |
# self._load_endonuclease_settings()
|
| 67 |
|
| 68 |
def _handle_reset(self):
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
def _add_job_to_table(self):
|
| 90 |
organism_name = self.view.get_organism_name()
|
|
@@ -123,16 +137,28 @@ class NewGenomeWindowController:
|
|
| 123 |
|
| 124 |
def _browse_fasta_file(self):
|
| 125 |
file_dialog = QtWidgets.QFileDialog()
|
| 126 |
-
database_dir = self.settings.
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
if file_path:
|
| 129 |
if self.model.validate_fasta_file(file_path):
|
| 130 |
self.model.file = file_path
|
| 131 |
self.view.set_selected_file(file_path)
|
|
|
|
| 132 |
else:
|
| 133 |
-
show_message(
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
def _remove_selected_job(self):
|
| 138 |
job_identifier = self.view.get_selected_job_identifier()
|
|
@@ -235,48 +261,73 @@ class NewGenomeWindowController:
|
|
| 235 |
self.view.table_widget_jobs.viewport().update()
|
| 236 |
|
| 237 |
def _handle_job_completion(self, exit_code=None, exit_status=None):
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
remaining_output = self.job_process.readAllStandardOutput().data().decode()
|
| 242 |
-
if remaining_output:
|
| 243 |
-
self.logger.debug(f"Final process output: {remaining_output}")
|
| 244 |
-
|
| 245 |
-
remaining_error = self.job_process.readAllStandardError().data().decode()
|
| 246 |
-
if remaining_error:
|
| 247 |
-
self.logger.error(f"Final process error output: {remaining_error}")
|
| 248 |
-
|
| 249 |
-
if hasattr(self, 'job_indexes') and self.job_indexes:
|
| 250 |
-
completed_row_index = self.job_indexes.pop(0)
|
| 251 |
-
|
| 252 |
-
# Check if output files were created
|
| 253 |
-
expected_cspr_file = os.path.join(self.settings.get_db_path(), f"{self.model.get_job_name(completed_row_index)}.cspr")
|
| 254 |
-
if os.path.exists(expected_cspr_file):
|
| 255 |
-
self.logger.debug(f"CSPR file created successfully: {expected_cspr_file}")
|
| 256 |
-
else:
|
| 257 |
-
self.logger.error(f"Expected CSPR file not found: {expected_cspr_file}")
|
| 258 |
|
| 259 |
-
#
|
| 260 |
-
self.
|
|
|
|
|
|
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
self.view.set_progress_bar_jobs(total_progress)
|
| 266 |
-
else:
|
| 267 |
-
self.logger.warning("Received None for total_progress")
|
| 268 |
|
| 269 |
-
if self.job_indexes:
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
else:
|
| 273 |
-
self.logger.
|
| 274 |
-
self.view.set_progress_bar_jobs(100)
|
| 275 |
-
self.settings.update_db_state()
|
| 276 |
-
else:
|
| 277 |
-
self.logger.warning("No job indexes found or all jobs completed")
|
| 278 |
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
def _reset_table_widget_jobs(self):
|
| 282 |
self.view.reset_table_widget_jobs()
|
|
@@ -293,10 +344,13 @@ class NewGenomeWindowController:
|
|
| 293 |
|
| 294 |
# Connect to the initialization complete signal
|
| 295 |
def on_init_complete():
|
|
|
|
| 296 |
if organism_name:
|
| 297 |
ncbi_controller.view.line_edit_organism.setText(organism_name)
|
| 298 |
if strain_name:
|
| 299 |
ncbi_controller.view.line_edit_strain.setText(strain_name)
|
|
|
|
|
|
|
| 300 |
|
| 301 |
# Connect the signal
|
| 302 |
ncbi_controller.view.initialization_complete.connect(on_init_complete)
|
|
@@ -308,9 +362,6 @@ class NewGenomeWindowController:
|
|
| 308 |
show_error(self.settings, "Error opening NCBI module", str(e))
|
| 309 |
self.logger.error(f"Failed to open NCBI module: {str(e)}")
|
| 310 |
|
| 311 |
-
# def _on_cspr_files_created(self):
|
| 312 |
-
# self.settings.update_db_state()
|
| 313 |
-
|
| 314 |
def _load_initial_endonuclease_settings(self):
|
| 315 |
initial_endonuclease = self.view.get_selected_endonuclease()
|
| 316 |
print(f"Initial endonuclease: {initial_endonuclease}")
|
|
|
|
| 3 |
from views.NewGenomeWindowView import NewGenomeWindowView
|
| 4 |
from utils.ui import show_message, show_error
|
| 5 |
import os
|
| 6 |
+
from PyQt6.QtCore import pyqtSignal
|
| 7 |
|
| 8 |
class NewGenomeWindowController:
|
| 9 |
def __init__(self, global_settings):
|
| 10 |
self.settings = global_settings
|
| 11 |
self.logger = global_settings.get_logger()
|
| 12 |
+
self.directory_change_completed = pyqtSignal(str)
|
| 13 |
|
| 14 |
try:
|
| 15 |
self.model = NewGenomeWindowModel(self.settings)
|
|
|
|
| 68 |
# self._load_endonuclease_settings()
|
| 69 |
|
| 70 |
def _handle_reset(self):
|
| 71 |
+
try:
|
| 72 |
+
self.view.line_edit_organism_name.clear()
|
| 73 |
+
self.view.line_edit_strain.clear()
|
| 74 |
+
self.view.line_edit_organism_code.clear()
|
| 75 |
|
| 76 |
+
self.model.file = ""
|
| 77 |
+
self.view.line_edit_selected_file.clear()
|
| 78 |
+
self.view.line_edit_selected_file.setPlaceholderText("Selected FASTA/FNA File")
|
| 79 |
|
| 80 |
+
self.view.reset_table_widget_jobs()
|
| 81 |
+
self.view.reset_progress_bar_jobs()
|
| 82 |
|
| 83 |
+
# Reinitialize the process
|
| 84 |
+
if self.job_process.state() != QtCore.QProcess.ProcessState.NotRunning:
|
| 85 |
+
self.job_process.kill()
|
| 86 |
+
self._initialize_process()
|
| 87 |
|
| 88 |
+
# Reset the model
|
| 89 |
+
self.model.reset_progress()
|
| 90 |
+
self.model.jobs.clear()
|
| 91 |
+
|
| 92 |
+
# Cancel any pending database path change
|
| 93 |
+
self.settings.db_manager.pending_db_path = None
|
| 94 |
+
self.logger.debug("Cancelled pending database path change")
|
| 95 |
+
|
| 96 |
+
# Show confirmation to user
|
| 97 |
+
show_message("Reset Complete",
|
| 98 |
+
"Form has been reset and any pending database changes have been cancelled.")
|
| 99 |
+
except Exception as e:
|
| 100 |
+
self.logger.error(f"Error in handle reset: {str(e)}")
|
| 101 |
+
show_error(self.settings, "Error", str(e))
|
| 102 |
|
| 103 |
def _add_job_to_table(self):
|
| 104 |
organism_name = self.view.get_organism_name()
|
|
|
|
| 137 |
|
| 138 |
def _browse_fasta_file(self):
|
| 139 |
file_dialog = QtWidgets.QFileDialog()
|
| 140 |
+
database_dir = self.settings.db_manager.get_active_db_path()
|
| 141 |
+
self.logger.debug(f"Opening file dialog with directory: {database_dir}")
|
| 142 |
+
|
| 143 |
+
file_path, _ = file_dialog.getOpenFileName(
|
| 144 |
+
self.view,
|
| 145 |
+
"Choose a File",
|
| 146 |
+
database_dir,
|
| 147 |
+
"FASTA Files (*.fa *.fna *.fasta)"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
if file_path:
|
| 151 |
if self.model.validate_fasta_file(file_path):
|
| 152 |
self.model.file = file_path
|
| 153 |
self.view.set_selected_file(file_path)
|
| 154 |
+
self.logger.debug(f"Selected valid FASTA file: {file_path}")
|
| 155 |
else:
|
| 156 |
+
show_message(
|
| 157 |
+
fontSize=12,
|
| 158 |
+
icon=QtWidgets.QMessageBox.Icon.Critical,
|
| 159 |
+
title="File Selection Error",
|
| 160 |
+
message="You have selected an incorrect type of file. Please choose a FASTA/FNA file."
|
| 161 |
+
)
|
| 162 |
|
| 163 |
def _remove_selected_job(self):
|
| 164 |
job_identifier = self.view.get_selected_job_identifier()
|
|
|
|
| 261 |
self.view.table_widget_jobs.viewport().update()
|
| 262 |
|
| 263 |
def _handle_job_completion(self, exit_code=None, exit_status=None):
|
| 264 |
+
"""Handle process completion and job status updates"""
|
| 265 |
+
try:
|
| 266 |
+
self.logger.debug(f"Process finished with exit code: {exit_code}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
+
# Log any remaining output
|
| 269 |
+
remaining_output = self.job_process.readAllStandardOutput().data().decode()
|
| 270 |
+
if remaining_output:
|
| 271 |
+
self.logger.debug(f"Final process output: {remaining_output}")
|
| 272 |
|
| 273 |
+
remaining_error = self.job_process.readAllStandardError().data().decode()
|
| 274 |
+
if remaining_error:
|
| 275 |
+
self.logger.error(f"Final process error output: {remaining_error}")
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
+
if hasattr(self, 'job_indexes') and self.job_indexes:
|
| 278 |
+
completed_row_index = self.job_indexes.pop(0)
|
| 279 |
+
|
| 280 |
+
# Get the job name for the completed job
|
| 281 |
+
job_name = self.model.get_job_name(completed_row_index)
|
| 282 |
+
expected_cspr_file = os.path.join(self.settings.get_db_path(), f"{job_name}.cspr")
|
| 283 |
+
|
| 284 |
+
if exit_code == 0 and os.path.exists(expected_cspr_file):
|
| 285 |
+
self.logger.debug(f"CSPR file created successfully: {expected_cspr_file}")
|
| 286 |
+
self.view.set_job_completed(completed_row_index)
|
| 287 |
+
|
| 288 |
+
# Update model's completed jobs count and progress bar
|
| 289 |
+
total_progress = self.model.increment_completed_jobs()
|
| 290 |
+
if total_progress is not None:
|
| 291 |
+
self.view.set_progress_bar_jobs(total_progress)
|
| 292 |
+
else:
|
| 293 |
+
self.logger.warning("Received None for total_progress")
|
| 294 |
+
|
| 295 |
+
# If we're in directory change mode, trigger a database state update
|
| 296 |
+
if self.settings.db_manager.is_changing_directory:
|
| 297 |
+
self.logger.debug("Directory change in progress - triggering state update")
|
| 298 |
+
self.settings.db_manager.update_db_state()
|
| 299 |
+
else:
|
| 300 |
+
error_msg = f"Job failed: CSPR file not found or process error (exit code: {exit_code})"
|
| 301 |
+
self.logger.error(error_msg)
|
| 302 |
+
show_error(self.settings, f"Job Failed: {job_name}", error_msg)
|
| 303 |
+
|
| 304 |
+
# Process next job if any
|
| 305 |
+
if self.job_indexes:
|
| 306 |
+
next_row_index = self.job_indexes[0]
|
| 307 |
+
self._run_job(next_row_index)
|
| 308 |
+
else:
|
| 309 |
+
self.logger.info("All queued jobs completed")
|
| 310 |
+
self.view.set_progress_bar_jobs(100)
|
| 311 |
+
|
| 312 |
+
# If we were changing directory, finalize the change
|
| 313 |
+
if self.settings.db_manager.is_changing_directory:
|
| 314 |
+
self.logger.debug("Finalizing directory change after successful genome analysis")
|
| 315 |
+
success, message = self.settings.db_manager.finalize_directory_change()
|
| 316 |
+
if success:
|
| 317 |
+
self.directory_change_completed.emit(message)
|
| 318 |
+
else:
|
| 319 |
+
self.logger.error(f"Failed to finalize directory change: {message}")
|
| 320 |
+
else:
|
| 321 |
+
# Just update the state if not changing directory
|
| 322 |
+
self.settings.update_db_state()
|
| 323 |
+
|
| 324 |
else:
|
| 325 |
+
self.logger.warning("No job indexes found or all jobs completed")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
+
self.view.table_widget_jobs.viewport().update()
|
| 328 |
+
|
| 329 |
+
except Exception as e:
|
| 330 |
+
self.logger.error(f"Error in _handle_job_completion: {str(e)}")
|
| 331 |
|
| 332 |
def _reset_table_widget_jobs(self):
|
| 333 |
self.view.reset_table_widget_jobs()
|
|
|
|
| 344 |
|
| 345 |
# Connect to the initialization complete signal
|
| 346 |
def on_init_complete():
|
| 347 |
+
self.logger.debug(f"NCBI window initialized, setting organism: {organism_name}, strain: {strain_name}")
|
| 348 |
if organism_name:
|
| 349 |
ncbi_controller.view.line_edit_organism.setText(organism_name)
|
| 350 |
if strain_name:
|
| 351 |
ncbi_controller.view.line_edit_strain.setText(strain_name)
|
| 352 |
+
# Disconnect after use to prevent multiple connections
|
| 353 |
+
ncbi_controller.view.initialization_complete.disconnect(on_init_complete)
|
| 354 |
|
| 355 |
# Connect the signal
|
| 356 |
ncbi_controller.view.initialization_complete.connect(on_init_complete)
|
|
|
|
| 362 |
show_error(self.settings, "Error opening NCBI module", str(e))
|
| 363 |
self.logger.error(f"Failed to open NCBI module: {str(e)}")
|
| 364 |
|
|
|
|
|
|
|
|
|
|
| 365 |
def _load_initial_endonuclease_settings(self):
|
| 366 |
initial_endonuclease = self.view.get_selected_endonuclease()
|
| 367 |
print(f"Initial endonuclease: {initial_endonuclease}")
|
|
@@ -218,6 +218,18 @@ class OffTargetController(QObject):
|
|
| 218 |
if endo_index >= 0:
|
| 219 |
self.view.combo_box_endonuclease.setCurrentIndex(endo_index)
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
# Store targets for analysis
|
| 222 |
if 'guides' in parameters:
|
| 223 |
self._targets = parameters['guides']
|
|
|
|
| 218 |
if endo_index >= 0:
|
| 219 |
self.view.combo_box_endonuclease.setCurrentIndex(endo_index)
|
| 220 |
|
| 221 |
+
# Validate and set annotation file
|
| 222 |
+
if 'annotation_file' not in parameters:
|
| 223 |
+
raise ValueError("No annotation file provided in parameters")
|
| 224 |
+
|
| 225 |
+
annotation_file = parameters['annotation_file']
|
| 226 |
+
if not annotation_file:
|
| 227 |
+
raise ValueError("Empty annotation file path provided")
|
| 228 |
+
|
| 229 |
+
# Set annotation file in global settings
|
| 230 |
+
self.global_settings.set_current_annotation_file(annotation_file)
|
| 231 |
+
self.logger.debug(f"Set annotation file to: {annotation_file}")
|
| 232 |
+
|
| 233 |
# Store targets for analysis
|
| 234 |
if 'guides' in parameters:
|
| 235 |
self._targets = parameters['guides']
|
|
@@ -39,7 +39,6 @@ class PopulationAnalysisWindowController:
|
|
| 39 |
def launch(self):
|
| 40 |
try:
|
| 41 |
self.logger = self.global_settings.get_logger()
|
| 42 |
-
self.logger.info("Launching Population Analysis Window")
|
| 43 |
self.get_data()
|
| 44 |
except Exception as e:
|
| 45 |
self.logger.error(f"Error in launch(): {str(e)}")
|
|
@@ -47,7 +46,6 @@ class PopulationAnalysisWindowController:
|
|
| 47 |
|
| 48 |
def get_data(self):
|
| 49 |
try:
|
| 50 |
-
self.logger.info("Getting data for Population Analysis")
|
| 51 |
self.fillEndo()
|
| 52 |
except Exception as e:
|
| 53 |
self.logger.error(f"Error in get_data(): {str(e)}")
|
|
@@ -55,7 +53,6 @@ class PopulationAnalysisWindowController:
|
|
| 55 |
|
| 56 |
def fillEndo(self):
|
| 57 |
try:
|
| 58 |
-
self.logger.info("Starting fillEndo()")
|
| 59 |
endos = self.model.load_endonucleases()
|
| 60 |
self.logger.debug(f"Loaded endonucleases: {endos}")
|
| 61 |
|
|
@@ -64,7 +61,6 @@ class PopulationAnalysisWindowController:
|
|
| 64 |
show_error(self.global_settings, "Error", "No endonucleases found")
|
| 65 |
return
|
| 66 |
|
| 67 |
-
self.logger.info(f"Updating dropdown with {len(endos)} endonucleases")
|
| 68 |
self.view.update_endo_dropdown(endos.keys())
|
| 69 |
self.change_endo()
|
| 70 |
except Exception as e:
|
|
@@ -190,8 +186,6 @@ class PopulationAnalysisWindowController:
|
|
| 190 |
data['pams'][majority_index], # PAM
|
| 191 |
strand # Strand
|
| 192 |
)
|
| 193 |
-
|
| 194 |
-
self.logger.debug(f"Processed seed data: {row_data}")
|
| 195 |
return row_data
|
| 196 |
|
| 197 |
except Exception as e:
|
|
|
|
| 39 |
def launch(self):
|
| 40 |
try:
|
| 41 |
self.logger = self.global_settings.get_logger()
|
|
|
|
| 42 |
self.get_data()
|
| 43 |
except Exception as e:
|
| 44 |
self.logger.error(f"Error in launch(): {str(e)}")
|
|
|
|
| 46 |
|
| 47 |
def get_data(self):
|
| 48 |
try:
|
|
|
|
| 49 |
self.fillEndo()
|
| 50 |
except Exception as e:
|
| 51 |
self.logger.error(f"Error in get_data(): {str(e)}")
|
|
|
|
| 53 |
|
| 54 |
def fillEndo(self):
|
| 55 |
try:
|
|
|
|
| 56 |
endos = self.model.load_endonucleases()
|
| 57 |
self.logger.debug(f"Loaded endonucleases: {endos}")
|
| 58 |
|
|
|
|
| 61 |
show_error(self.global_settings, "Error", "No endonucleases found")
|
| 62 |
return
|
| 63 |
|
|
|
|
| 64 |
self.view.update_endo_dropdown(endos.keys())
|
| 65 |
self.change_endo()
|
| 66 |
except Exception as e:
|
|
|
|
| 186 |
data['pams'][majority_index], # PAM
|
| 187 |
strand # Strand
|
| 188 |
)
|
|
|
|
|
|
|
| 189 |
return row_data
|
| 190 |
|
| 191 |
except Exception as e:
|
|
@@ -6,10 +6,11 @@ from views.StartupWindowView import StartupWindowView
|
|
| 6 |
import sys
|
| 7 |
|
| 8 |
class StartupWindowController:
|
| 9 |
-
def __init__(self, global_settings):
|
| 10 |
self.settings = global_settings
|
| 11 |
self.logger = self.settings.get_logger()
|
| 12 |
-
self.is_active = True
|
|
|
|
| 13 |
|
| 14 |
try:
|
| 15 |
self.view = StartupWindowView(self.settings)
|
|
@@ -31,10 +32,19 @@ class StartupWindowController:
|
|
| 31 |
self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
|
| 32 |
self.model.db_state_updated.connect(self._on_db_state_updated)
|
| 33 |
self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
|
|
|
|
| 34 |
self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
|
| 35 |
|
| 36 |
def _on_db_path_text_changed(self, new_path):
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
def _on_db_state_updated(self, is_valid, message, cspr_files):
|
| 40 |
if self.is_active and hasattr(self, 'view'):
|
|
@@ -45,15 +55,63 @@ class StartupWindowController:
|
|
| 45 |
if self.is_active and hasattr(self, 'view'):
|
| 46 |
self.view.set_db_status(is_valid, message)
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
def _init_ui(self):
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
def _init_db_state(self, db_path):
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
def _set_database_directory(self):
|
| 59 |
try:
|
|
|
|
| 6 |
import sys
|
| 7 |
|
| 8 |
class StartupWindowController:
|
| 9 |
+
def __init__(self, global_settings, keep_db_path=False):
|
| 10 |
self.settings = global_settings
|
| 11 |
self.logger = self.settings.get_logger()
|
| 12 |
+
self.is_active = True
|
| 13 |
+
self.keep_db_path = keep_db_path
|
| 14 |
|
| 15 |
try:
|
| 16 |
self.view = StartupWindowView(self.settings)
|
|
|
|
| 32 |
self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
|
| 33 |
self.model.db_state_updated.connect(self._on_db_state_updated)
|
| 34 |
self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
|
| 35 |
+
self.settings.db_manager.db_files_changed.connect(self._on_db_files_changed)
|
| 36 |
self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
|
| 37 |
|
| 38 |
def _on_db_path_text_changed(self, new_path):
|
| 39 |
+
"""Handle database path text changes"""
|
| 40 |
+
try:
|
| 41 |
+
# Avoid triggering save if the path hasn't actually changed
|
| 42 |
+
if new_path == self.model.get_db_path():
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
self.model.save_db_path(new_path)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
self.logger.error(f"Error handling db path change: {str(e)}")
|
| 48 |
|
| 49 |
def _on_db_state_updated(self, is_valid, message, cspr_files):
|
| 50 |
if self.is_active and hasattr(self, 'view'):
|
|
|
|
| 55 |
if self.is_active and hasattr(self, 'view'):
|
| 56 |
self.view.set_db_status(is_valid, message)
|
| 57 |
|
| 58 |
+
def _on_db_files_changed(self, changes):
|
| 59 |
+
"""Handle database file changes"""
|
| 60 |
+
if self.is_active and hasattr(self, 'view'):
|
| 61 |
+
# Re-validate the current path
|
| 62 |
+
db_path = self.model.get_db_path()
|
| 63 |
+
is_valid, message = self.settings.validate_db_path(db_path)
|
| 64 |
+
self.view.set_db_status(is_valid, message)
|
| 65 |
+
|
| 66 |
+
# If path is now valid and we have CSPR files, update button text
|
| 67 |
+
if is_valid:
|
| 68 |
+
self.view.push_button_go_to_home_or_new_genome.setText("Go to Home")
|
| 69 |
+
|
| 70 |
def _init_ui(self):
|
| 71 |
+
"""Initialize the UI with the correct database path"""
|
| 72 |
+
try:
|
| 73 |
+
# Always get the current path from settings
|
| 74 |
+
db_path = self.model.get_db_path()
|
| 75 |
+
|
| 76 |
+
# For true first time startup (no previous path), show default path
|
| 77 |
+
if self.settings.is_first_time_startup and not db_path:
|
| 78 |
+
db_path = self.settings.db_manager.get_default_database_path()
|
| 79 |
+
# For invalid path case, keep the existing path
|
| 80 |
+
elif not self.keep_db_path and not self.settings.is_first_time_startup:
|
| 81 |
+
db_path = ''
|
| 82 |
+
|
| 83 |
+
self.logger.debug(f"Initial database path: {db_path}, keep_db_path: {self.keep_db_path}, "
|
| 84 |
+
f"first_time_startup: {self.settings.is_first_time_startup}")
|
| 85 |
+
|
| 86 |
+
# Set the path in the view first
|
| 87 |
+
self.view.set_db_path(db_path)
|
| 88 |
+
|
| 89 |
+
# Then initialize the state
|
| 90 |
+
if db_path:
|
| 91 |
+
is_valid, message = self.settings.validate_db_path(db_path)
|
| 92 |
+
self.view.set_db_status(is_valid, message)
|
| 93 |
+
else:
|
| 94 |
+
self.view.set_db_status(False, "No directory selected")
|
| 95 |
+
|
| 96 |
+
except Exception as e:
|
| 97 |
+
self.logger.error(f"Error in _init_ui: {str(e)}")
|
| 98 |
+
raise
|
| 99 |
|
| 100 |
def _init_db_state(self, db_path):
|
| 101 |
+
"""Initialize database state"""
|
| 102 |
+
try:
|
| 103 |
+
# Only update the view's path if it's different from current
|
| 104 |
+
current_view_path = self.view.get_db_path()
|
| 105 |
+
if current_view_path != db_path:
|
| 106 |
+
self.view.set_db_path(db_path)
|
| 107 |
+
|
| 108 |
+
# Validate and update status
|
| 109 |
+
is_valid, message = self.settings.validate_db_path(db_path)
|
| 110 |
+
self.view.set_db_status(is_valid, message)
|
| 111 |
+
|
| 112 |
+
except Exception as e:
|
| 113 |
+
self.logger.error(f"Error in _init_db_state: {str(e)}")
|
| 114 |
+
raise
|
| 115 |
|
| 116 |
def _set_database_directory(self):
|
| 117 |
try:
|
|
@@ -8,6 +8,7 @@ import traceback
|
|
| 8 |
from views.LoadingDialog import LoadingDialog
|
| 9 |
from PyQt6.QtWidgets import QApplication
|
| 10 |
from PyQt6.QtGui import QColor
|
|
|
|
| 11 |
|
| 12 |
class ViewTargetsController:
|
| 13 |
def __init__(self, global_settings):
|
|
@@ -31,7 +32,6 @@ class ViewTargetsController:
|
|
| 31 |
self.view.push_button_change_location.clicked.connect(self.change_indices)
|
| 32 |
self.view.push_button_reset_location.clicked.connect(self.reset_location)
|
| 33 |
self.view.check_box_select_all.stateChanged.connect(self.select_all)
|
| 34 |
-
# self.view.combo_box_gene.currentIndexChanged.connect(self.display_gene_data)
|
| 35 |
self.view.gene_selected.connect(self.on_gene_selected)
|
| 36 |
|
| 37 |
self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
|
|
@@ -42,12 +42,34 @@ class ViewTargetsController:
|
|
| 42 |
"""Handle view exons only checkbox state change"""
|
| 43 |
try:
|
| 44 |
is_checked = self.view.check_box_view_exons_only.isChecked()
|
| 45 |
-
self.logger.debug(f"View exons only changed to: {is_checked}")
|
| 46 |
self.model.set_view_exons_only(is_checked)
|
| 47 |
self.refresh_gene_viewer()
|
| 48 |
except Exception as e:
|
| 49 |
self.logger.error(f"Error handling view exons change: {str(e)}")
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def load_guides(self, selected_targets, organism, endonuclease, loading_dialog=None):
|
| 52 |
try:
|
| 53 |
self.organism = organism
|
|
@@ -62,6 +84,9 @@ class ViewTargetsController:
|
|
| 62 |
QApplication.processEvents()
|
| 63 |
|
| 64 |
try:
|
|
|
|
|
|
|
|
|
|
| 65 |
loading_dialog.set_message("Loading guides...", 60)
|
| 66 |
QApplication.processEvents()
|
| 67 |
|
|
@@ -92,43 +117,73 @@ class ViewTargetsController:
|
|
| 92 |
loading_dialog.set_message("Updating display...", 80)
|
| 93 |
QApplication.processEvents()
|
| 94 |
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
#
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
unique_entries = set()
|
| 104 |
-
for target in selected_targets:
|
| 105 |
-
if 'feature_id' in target:
|
| 106 |
-
# For position-based searches, use the feature_id directly
|
| 107 |
-
if "chromosome" in str(target['feature_id']):
|
| 108 |
-
unique_entries.add(target['feature_id'])
|
| 109 |
-
else:
|
| 110 |
-
# For gene-based searches, get gene data and format with name
|
| 111 |
-
locus_tag = target['feature_id']
|
| 112 |
-
gene_data = self.model.get_gene_data(locus_tag)
|
| 113 |
-
if gene_data and 'info' in gene_data:
|
| 114 |
-
gene_name = gene_data['info'].get('gene_name', '')
|
| 115 |
-
display_text = f"{locus_tag}: {gene_name}" if gene_name else locus_tag
|
| 116 |
-
unique_entries.add(display_text)
|
| 117 |
|
| 118 |
-
#
|
| 119 |
-
|
| 120 |
-
self.logger.debug(f"Found {len(entries)} unique entries")
|
| 121 |
|
|
|
|
| 122 |
self.view.set_combo_box_gene(entries)
|
|
|
|
| 123 |
|
| 124 |
-
#
|
| 125 |
-
if
|
| 126 |
-
|
| 127 |
-
self.view.
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
|
|
|
|
| 132 |
|
| 133 |
loading_dialog.set_progress(100)
|
| 134 |
QApplication.processEvents()
|
|
@@ -144,53 +199,6 @@ class ViewTargetsController:
|
|
| 144 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 145 |
show_error(self.settings, "Error loading guides", str(e))
|
| 146 |
|
| 147 |
-
def _load_initial_gene_data(self, selected_text):
|
| 148 |
-
"""Load initial gene data without showing loading dialog"""
|
| 149 |
-
try:
|
| 150 |
-
# Similar to on_gene_selected but without loading dialog
|
| 151 |
-
if "chromosome" in selected_text and "start:" in selected_text:
|
| 152 |
-
# Parse position from the text
|
| 153 |
-
parts = selected_text.split(',')
|
| 154 |
-
chrom = parts[0].split('chromosome')[1].strip() # Remove any extra colons
|
| 155 |
-
start = int(parts[1].split('start:')[1].strip())
|
| 156 |
-
end = int(parts[2].split('end:')[1].strip())
|
| 157 |
-
|
| 158 |
-
self.view.line_edit_start_location.setText(str(start))
|
| 159 |
-
self.view.line_edit_stop_location.setText(str(end))
|
| 160 |
-
|
| 161 |
-
# Get sequence directly for position-based search
|
| 162 |
-
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 163 |
-
if sequence:
|
| 164 |
-
# Update gene viewer with sequence
|
| 165 |
-
self.view.update_gene_viewer(sequence, [])
|
| 166 |
-
self.logger.debug(f"Updated gene viewer with sequence of length {len(sequence)}")
|
| 167 |
-
else:
|
| 168 |
-
self.logger.error(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
|
| 169 |
-
|
| 170 |
-
position_guides = [g for g in self.model.guides
|
| 171 |
-
if g.get('feature_id') == selected_text]
|
| 172 |
-
self.view.display_guides_in_table(position_guides)
|
| 173 |
-
else:
|
| 174 |
-
# Regular gene-based search
|
| 175 |
-
locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 176 |
-
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 177 |
-
if sequence_data:
|
| 178 |
-
self.view.line_edit_start_location.setText(str(sequence_data['start']))
|
| 179 |
-
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 180 |
-
|
| 181 |
-
features = self.model.get_features_for_gene(locus_tag)
|
| 182 |
-
|
| 183 |
-
self.view.update_gene_viewer(sequence_data['sequence'], features)
|
| 184 |
-
|
| 185 |
-
gene_guides = [g for g in self.model.guides
|
| 186 |
-
if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
|
| 187 |
-
self.view.display_guides_in_table(gene_guides)
|
| 188 |
-
|
| 189 |
-
self.logger.debug("Initial gene data loaded successfully")
|
| 190 |
-
|
| 191 |
-
except Exception as e:
|
| 192 |
-
self.logger.error(f"Error loading initial gene data: {str(e)}")
|
| 193 |
-
|
| 194 |
def _on_endonuclease_changed(self, new_endonuclease):
|
| 195 |
try:
|
| 196 |
if new_endonuclease != self.endonuclease:
|
|
@@ -210,8 +218,6 @@ class ViewTargetsController:
|
|
| 210 |
new_target['endonuclease'] = new_endonuclease
|
| 211 |
updated_targets.append(new_target)
|
| 212 |
|
| 213 |
-
self.logger.debug(f"Created {len(updated_targets)} updated targets for {new_endonuclease}")
|
| 214 |
-
|
| 215 |
# Update model with new targets
|
| 216 |
self.model.load_guides(updated_targets, self.organism, new_endonuclease)
|
| 217 |
guides = self.model.get_guides()
|
|
@@ -226,36 +232,6 @@ class ViewTargetsController:
|
|
| 226 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 227 |
show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
|
| 228 |
|
| 229 |
-
# def load_gene_viewer(self):
|
| 230 |
-
# try:
|
| 231 |
-
# # Get selected gene from combo box
|
| 232 |
-
# selected_text = self.view.combo_box_gene.currentText()
|
| 233 |
-
# if not selected_text:
|
| 234 |
-
# self.logger.debug("No gene selected")
|
| 235 |
-
# return
|
| 236 |
-
|
| 237 |
-
# # Extract locus tag from "locus_tag: gene_name" format
|
| 238 |
-
# locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 239 |
-
# self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
|
| 240 |
-
|
| 241 |
-
# # Get gene sequence with padding
|
| 242 |
-
# sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 243 |
-
|
| 244 |
-
# if sequence_data:
|
| 245 |
-
# # Update gene viewer with sequence
|
| 246 |
-
# self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
|
| 247 |
-
|
| 248 |
-
# # Update location fields
|
| 249 |
-
# self.view.line_edit_start_location.setText(str(sequence_data['start']))
|
| 250 |
-
# self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 251 |
-
|
| 252 |
-
# else:
|
| 253 |
-
# self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
|
| 254 |
-
|
| 255 |
-
# except Exception as e:
|
| 256 |
-
# self.logger.error(f"Error in load_gene_viewer: {str(e)}")
|
| 257 |
-
# self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 258 |
-
|
| 259 |
def perform_off_target_analysis(self):
|
| 260 |
"""Launch off-target analysis for selected guides"""
|
| 261 |
try:
|
|
@@ -279,11 +255,35 @@ class ViewTargetsController:
|
|
| 279 |
self._handle_off_target_results
|
| 280 |
)
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
# Set initial parameters based on current organism/endonuclease
|
| 283 |
parameters = {
|
| 284 |
'organism': self.organism,
|
| 285 |
'endonuclease': self.endonuclease,
|
| 286 |
-
'guides': selected_guides # Pass the selected guides
|
|
|
|
| 287 |
}
|
| 288 |
|
| 289 |
# Initialize analysis with parameters
|
|
@@ -473,20 +473,9 @@ class ViewTargetsController:
|
|
| 473 |
show_error(self.settings, "Error", f"Could not show scoring options: {str(e)}")
|
| 474 |
|
| 475 |
def change_indices(self):
|
| 476 |
-
"""Change the
|
| 477 |
try:
|
| 478 |
-
#
|
| 479 |
-
if not self.view.text_edit_gene_viewer.toPlainText():
|
| 480 |
-
QMessageBox.warning(
|
| 481 |
-
self.view,
|
| 482 |
-
"Gene Viewer Error",
|
| 483 |
-
"Gene Viewer display is empty! Please ensure there is sequence data to view."
|
| 484 |
-
)
|
| 485 |
-
return
|
| 486 |
-
|
| 487 |
-
# Get current gene/position info
|
| 488 |
-
current_gene = self.view.combo_box_gene.currentText()
|
| 489 |
-
|
| 490 |
try:
|
| 491 |
new_start = int(self.view.line_edit_start_location.text())
|
| 492 |
new_end = int(self.view.line_edit_stop_location.text())
|
|
@@ -523,7 +512,10 @@ class ViewTargetsController:
|
|
| 523 |
)
|
| 524 |
return
|
| 525 |
|
| 526 |
-
# Get
|
|
|
|
|
|
|
|
|
|
| 527 |
if "chromosome" in current_gene and "start:" in current_gene:
|
| 528 |
# For position-based searches
|
| 529 |
try:
|
|
@@ -531,12 +523,14 @@ class ViewTargetsController:
|
|
| 531 |
# Get full chromosome identifier instead of just the number
|
| 532 |
chrom = parts[0].split('chromosome')[1].strip() # This will now keep the full identifier
|
| 533 |
|
| 534 |
-
# Get sequence for new range
|
| 535 |
-
sequence = self.model._get_sequence_for_position(chrom, new_start, new_end)
|
| 536 |
|
| 537 |
if sequence:
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
| 540 |
self.view.line_edit_start_location.setText(str(new_start))
|
| 541 |
self.view.line_edit_stop_location.setText(str(new_end))
|
| 542 |
else:
|
|
@@ -545,44 +539,60 @@ class ViewTargetsController:
|
|
| 545 |
except Exception as e:
|
| 546 |
QMessageBox.warning(
|
| 547 |
self.view,
|
| 548 |
-
"
|
| 549 |
-
f"
|
| 550 |
)
|
| 551 |
-
return
|
| 552 |
else:
|
| 553 |
-
# For
|
| 554 |
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
| 555 |
-
gene_data = self.model.get_gene_data(locus_tag)
|
| 556 |
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
self.view,
|
| 560 |
-
"Gene Data Error",
|
| 561 |
-
"Could not get gene data for the current selection."
|
| 562 |
-
)
|
| 563 |
-
return
|
| 564 |
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
else:
|
| 575 |
QMessageBox.warning(
|
| 576 |
self.view,
|
| 577 |
-
"
|
| 578 |
-
"Could not get sequence for
|
| 579 |
)
|
| 580 |
-
|
| 581 |
-
|
| 582 |
except Exception as e:
|
| 583 |
-
self.logger.error(f"Error
|
| 584 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 585 |
-
show_error(self.settings, "Error
|
| 586 |
|
| 587 |
def reset_location(self):
|
| 588 |
"""Reset gene viewer to the original sequence and location"""
|
|
@@ -602,12 +612,17 @@ class ViewTargetsController:
|
|
| 602 |
# Get sequence directly using model's method
|
| 603 |
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 604 |
if sequence:
|
| 605 |
-
# Update
|
| 606 |
-
self.view.
|
| 607 |
|
| 608 |
-
# Update location fields -
|
| 609 |
self.view.line_edit_start_location.setText(str(start + 1))
|
| 610 |
self.view.line_edit_stop_location.setText(str(end))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
else:
|
| 612 |
raise ValueError("Could not get sequence for position")
|
| 613 |
|
|
@@ -620,30 +635,41 @@ class ViewTargetsController:
|
|
| 620 |
)
|
| 621 |
return
|
| 622 |
else:
|
| 623 |
-
# For
|
| 624 |
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
| 625 |
-
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 626 |
|
|
|
|
|
|
|
| 627 |
if sequence_data:
|
| 628 |
-
#
|
| 629 |
-
self.
|
| 630 |
|
| 631 |
-
# Update
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
|
| 633 |
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
else:
|
| 635 |
-
self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
|
| 636 |
QMessageBox.warning(
|
| 637 |
self.view,
|
| 638 |
-
"Gene
|
| 639 |
-
"Could not get
|
| 640 |
)
|
| 641 |
-
|
| 642 |
-
|
| 643 |
except Exception as e:
|
| 644 |
self.logger.error(f"Error in reset_location: {str(e)}")
|
| 645 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 646 |
-
show_error(self.settings, "Error
|
| 647 |
|
| 648 |
def select_all(self, state):
|
| 649 |
try:
|
|
@@ -673,6 +699,10 @@ class ViewTargetsController:
|
|
| 673 |
# Get current guides from model
|
| 674 |
self.model.load_guides(self.selected_targets, self.organism, self.endonuclease)
|
| 675 |
guides = self.model.get_guides()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
self.view.display_guides_in_table(guides)
|
| 677 |
except Exception as e:
|
| 678 |
show_error(self.settings, "Error refreshing guides display", str(e))
|
|
@@ -689,6 +719,9 @@ class ViewTargetsController:
|
|
| 689 |
QApplication.processEvents()
|
| 690 |
|
| 691 |
try:
|
|
|
|
|
|
|
|
|
|
| 692 |
# Load data in chunks
|
| 693 |
loading_dialog.set_message("Loading sequence data...", 30)
|
| 694 |
QApplication.processEvents()
|
|
@@ -704,33 +737,41 @@ class ViewTargetsController:
|
|
| 704 |
self.view.line_edit_start_location.setText(str(start))
|
| 705 |
self.view.line_edit_stop_location.setText(str(end))
|
| 706 |
|
| 707 |
-
#
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
self.view.display_guides_in_table(position_guides)
|
| 713 |
|
| 714 |
else:
|
| 715 |
-
# Regular gene-based search
|
| 716 |
locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 717 |
-
|
| 718 |
|
| 719 |
-
if
|
| 720 |
-
self.view.line_edit_start_location.setText(str(
|
| 721 |
-
self.view.line_edit_stop_location.setText(str(
|
| 722 |
|
| 723 |
loading_dialog.set_message("Updating display...", 80)
|
| 724 |
QApplication.processEvents()
|
| 725 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
gene_guides = [g for g in self.model.guides
|
| 727 |
-
|
| 728 |
self.view.display_guides_in_table(gene_guides)
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
loading_dialog.set_message("Refreshing viewer...", 90)
|
| 732 |
-
QApplication.processEvents()
|
| 733 |
-
self.refresh_gene_viewer()
|
| 734 |
|
| 735 |
finally:
|
| 736 |
loading_dialog.close()
|
|
@@ -781,19 +822,22 @@ class ViewTargetsController:
|
|
| 781 |
print("Negative strand")
|
| 782 |
# Convert sequence to complement for negative strand search
|
| 783 |
print(f"Sequence: {sequence_upper}")
|
| 784 |
-
complement_sequence = ''.join({'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G',
|
|
|
|
|
|
|
| 785 |
print(f"Complement sequence: {complement_sequence}")
|
| 786 |
target_sequence = guide_sequence.upper()
|
| 787 |
print(f"Target sequence: {target_sequence}")
|
| 788 |
-
target_sequence = target_sequence[::-1]
|
| 789 |
print(f"Reversed target sequence: {target_sequence}")
|
| 790 |
pos = complement_sequence.find(target_sequence)
|
| 791 |
print(f"Position: {pos}")
|
|
|
|
| 792 |
if pos != -1:
|
| 793 |
color = QColor(255, 0, 0, 100) # Red for negative strand
|
| 794 |
self.logger.debug(f"Found negative strand sequence at position {pos}")
|
| 795 |
|
| 796 |
-
#
|
| 797 |
self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
|
| 798 |
pos,
|
| 799 |
pos + len(guide_sequence) - 1,
|
|
@@ -1079,12 +1123,25 @@ class ViewTargetsController:
|
|
| 1079 |
chrom = parts[0].split('chromosome')[1].strip()
|
| 1080 |
start = int(parts[1].split('start:')[1].strip())
|
| 1081 |
end = int(parts[2].split('end:')[1].strip())
|
|
|
|
|
|
|
| 1082 |
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 1083 |
|
| 1084 |
if sequence:
|
| 1085 |
# Get features for this region
|
| 1086 |
features = self.model.get_features_for_region(chrom, start, end)
|
| 1087 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1088 |
else:
|
| 1089 |
# Regular gene-based search
|
| 1090 |
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
|
@@ -1094,8 +1151,22 @@ class ViewTargetsController:
|
|
| 1094 |
if sequence_data and 'sequence' in sequence_data:
|
| 1095 |
# Get features for this gene
|
| 1096 |
features = self.model.get_features_for_gene(locus_tag)
|
| 1097 |
-
|
| 1098 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1099 |
else:
|
| 1100 |
self.logger.warning("No sequence data available")
|
| 1101 |
|
|
|
|
| 8 |
from views.LoadingDialog import LoadingDialog
|
| 9 |
from PyQt6.QtWidgets import QApplication
|
| 10 |
from PyQt6.QtGui import QColor
|
| 11 |
+
import os
|
| 12 |
|
| 13 |
class ViewTargetsController:
|
| 14 |
def __init__(self, global_settings):
|
|
|
|
| 32 |
self.view.push_button_change_location.clicked.connect(self.change_indices)
|
| 33 |
self.view.push_button_reset_location.clicked.connect(self.reset_location)
|
| 34 |
self.view.check_box_select_all.stateChanged.connect(self.select_all)
|
|
|
|
| 35 |
self.view.gene_selected.connect(self.on_gene_selected)
|
| 36 |
|
| 37 |
self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
|
|
|
|
| 42 |
"""Handle view exons only checkbox state change"""
|
| 43 |
try:
|
| 44 |
is_checked = self.view.check_box_view_exons_only.isChecked()
|
|
|
|
| 45 |
self.model.set_view_exons_only(is_checked)
|
| 46 |
self.refresh_gene_viewer()
|
| 47 |
except Exception as e:
|
| 48 |
self.logger.error(f"Error handling view exons change: {str(e)}")
|
| 49 |
|
| 50 |
+
def _clear_viewer_state(self):
|
| 51 |
+
"""Clear all highlights and cursor states from the gene viewer"""
|
| 52 |
+
try:
|
| 53 |
+
if hasattr(self.view, 'dna_feature_viewer'):
|
| 54 |
+
# Clear sequence viewer highlights and cursor
|
| 55 |
+
self.view.dna_feature_viewer.sequence_viewer.clear_highlights()
|
| 56 |
+
for nuc in self.view.dna_feature_viewer.sequence_viewer.nucleotides:
|
| 57 |
+
nuc.show_cursor = False
|
| 58 |
+
nuc.update()
|
| 59 |
+
self.view.dna_feature_viewer.sequence_viewer.selection_active = False
|
| 60 |
+
self.view.dna_feature_viewer.sequence_viewer.selection_start = None
|
| 61 |
+
self.view.dna_feature_viewer.sequence_viewer.selection_end = None
|
| 62 |
+
|
| 63 |
+
# Clear insertion zone cursor
|
| 64 |
+
if hasattr(self.view.dna_feature_viewer, 'insertion_zone'):
|
| 65 |
+
if hasattr(self.view.dna_feature_viewer.insertion_zone, 'sequence_cursor'):
|
| 66 |
+
self.view.dna_feature_viewer.insertion_zone.sequence_cursor.hide()
|
| 67 |
+
self.view.dna_feature_viewer.insertion_zone.current_cursor_pos = None
|
| 68 |
+
self.view.dna_feature_viewer.insertion_zone.selection_start = None
|
| 69 |
+
self.view.dna_feature_viewer.insertion_zone.selection_end = None
|
| 70 |
+
except Exception as e:
|
| 71 |
+
self.logger.error(f"Error clearing viewer state: {str(e)}")
|
| 72 |
+
|
| 73 |
def load_guides(self, selected_targets, organism, endonuclease, loading_dialog=None):
|
| 74 |
try:
|
| 75 |
self.organism = organism
|
|
|
|
| 84 |
QApplication.processEvents()
|
| 85 |
|
| 86 |
try:
|
| 87 |
+
# Clear any existing highlights and cursor
|
| 88 |
+
self._clear_viewer_state()
|
| 89 |
+
|
| 90 |
loading_dialog.set_message("Loading guides...", 60)
|
| 91 |
QApplication.processEvents()
|
| 92 |
|
|
|
|
| 117 |
loading_dialog.set_message("Updating display...", 80)
|
| 118 |
QApplication.processEvents()
|
| 119 |
|
| 120 |
+
# Get unique position names or gene IDs
|
| 121 |
+
unique_entries = set()
|
| 122 |
+
for target in selected_targets:
|
| 123 |
+
if 'feature_id' in target:
|
| 124 |
+
if "chromosome" in str(target['feature_id']):
|
| 125 |
+
unique_entries.add(target['feature_id'])
|
| 126 |
+
else:
|
| 127 |
+
locus_tag = target['feature_id']
|
| 128 |
+
gene_data = self.model.get_gene_data(locus_tag)
|
| 129 |
+
if gene_data and 'info' in gene_data:
|
| 130 |
+
gene_name = gene_data['info'].get('gene_name', '')
|
| 131 |
+
display_text = f"{locus_tag}: {gene_name}" if gene_name else locus_tag
|
| 132 |
+
unique_entries.add(display_text)
|
| 133 |
|
| 134 |
+
# Convert set to list for combo box
|
| 135 |
+
entries = list(unique_entries)
|
| 136 |
+
self.logger.debug(f"Found {len(entries)} unique entries")
|
| 137 |
+
|
| 138 |
+
if entries:
|
| 139 |
+
first_entry = entries[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
+
# Block signals during setup
|
| 142 |
+
self.view.combo_box_gene.blockSignals(True)
|
|
|
|
| 143 |
|
| 144 |
+
# Update combo box
|
| 145 |
self.view.set_combo_box_gene(entries)
|
| 146 |
+
self.view.combo_box_gene.setCurrentIndex(0)
|
| 147 |
|
| 148 |
+
# Display guides for the first entry
|
| 149 |
+
if "chromosome" in first_entry and "start:" in first_entry:
|
| 150 |
+
position_guides = [g for g in guides if g.get('feature_id') == first_entry]
|
| 151 |
+
self.view.display_guides_in_table(position_guides)
|
| 152 |
+
|
| 153 |
+
# Parse position from the text
|
| 154 |
+
parts = first_entry.split(',')
|
| 155 |
+
chrom = parts[0].split('chromosome')[1].strip()
|
| 156 |
+
start = int(parts[1].split('start:')[1].strip())
|
| 157 |
+
end = int(parts[2].split('end:')[1].strip())
|
| 158 |
+
|
| 159 |
+
# Update location fields - add 1 to start for display
|
| 160 |
+
self.view.line_edit_start_location.setText(str(start + 1))
|
| 161 |
+
self.view.line_edit_stop_location.setText(str(end))
|
| 162 |
+
|
| 163 |
+
# Get sequence directly for position-based search
|
| 164 |
+
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 165 |
+
if sequence:
|
| 166 |
+
# Single call to set_data
|
| 167 |
+
self.view.dna_feature_viewer.set_data(sequence, [], start)
|
| 168 |
+
else:
|
| 169 |
+
# Regular gene-based search
|
| 170 |
+
locus_tag = first_entry.split(': ')[0] if ': ' in first_entry else first_entry
|
| 171 |
+
gene_guides = [g for g in guides if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
|
| 172 |
+
self.view.display_guides_in_table(gene_guides)
|
| 173 |
+
|
| 174 |
+
# Get sequence data and features
|
| 175 |
+
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 176 |
+
if sequence_data:
|
| 177 |
+
# Update location fields - add 1 to start for display
|
| 178 |
+
self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
|
| 179 |
+
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 180 |
+
|
| 181 |
+
# Get features and update viewer in a single call
|
| 182 |
+
features = self.model.get_features_for_gene(locus_tag)
|
| 183 |
+
self.view.dna_feature_viewer.set_data(sequence_data['sequence'], features, sequence_data['start'])
|
| 184 |
|
| 185 |
+
# Now unblock signals after everything is set up
|
| 186 |
+
self.view.combo_box_gene.blockSignals(False)
|
| 187 |
|
| 188 |
loading_dialog.set_progress(100)
|
| 189 |
QApplication.processEvents()
|
|
|
|
| 199 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 200 |
show_error(self.settings, "Error loading guides", str(e))
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
def _on_endonuclease_changed(self, new_endonuclease):
|
| 203 |
try:
|
| 204 |
if new_endonuclease != self.endonuclease:
|
|
|
|
| 218 |
new_target['endonuclease'] = new_endonuclease
|
| 219 |
updated_targets.append(new_target)
|
| 220 |
|
|
|
|
|
|
|
| 221 |
# Update model with new targets
|
| 222 |
self.model.load_guides(updated_targets, self.organism, new_endonuclease)
|
| 223 |
guides = self.model.get_guides()
|
|
|
|
| 232 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 233 |
show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
def perform_off_target_analysis(self):
|
| 236 |
"""Launch off-target analysis for selected guides"""
|
| 237 |
try:
|
|
|
|
| 255 |
self._handle_off_target_results
|
| 256 |
)
|
| 257 |
|
| 258 |
+
# Get current annotation file
|
| 259 |
+
current_annotation_file = self.settings.get_current_annotation_file()
|
| 260 |
+
if not current_annotation_file:
|
| 261 |
+
QtWidgets.QMessageBox.warning(
|
| 262 |
+
self.view,
|
| 263 |
+
"No Annotation File",
|
| 264 |
+
"Please select an annotation file before performing off-target analysis."
|
| 265 |
+
)
|
| 266 |
+
return
|
| 267 |
+
|
| 268 |
+
# Verify annotation file exists
|
| 269 |
+
annotation_path = os.path.join(self.settings.get_db_path(), 'GBFF', current_annotation_file)
|
| 270 |
+
if not os.path.isfile(annotation_path):
|
| 271 |
+
# Try without GBFF subdirectory
|
| 272 |
+
annotation_path = os.path.join(self.settings.get_db_path(), current_annotation_file)
|
| 273 |
+
if not os.path.isfile(annotation_path):
|
| 274 |
+
QtWidgets.QMessageBox.warning(
|
| 275 |
+
self.view,
|
| 276 |
+
"Invalid Annotation File",
|
| 277 |
+
f"Could not find annotation file at {annotation_path}"
|
| 278 |
+
)
|
| 279 |
+
return
|
| 280 |
+
|
| 281 |
# Set initial parameters based on current organism/endonuclease
|
| 282 |
parameters = {
|
| 283 |
'organism': self.organism,
|
| 284 |
'endonuclease': self.endonuclease,
|
| 285 |
+
'guides': selected_guides, # Pass the selected guides
|
| 286 |
+
'annotation_file': current_annotation_file # Add annotation file
|
| 287 |
}
|
| 288 |
|
| 289 |
# Initialize analysis with parameters
|
|
|
|
| 473 |
show_error(self.settings, "Error", f"Could not show scoring options: {str(e)}")
|
| 474 |
|
| 475 |
def change_indices(self):
|
| 476 |
+
"""Change the displayed sequence range"""
|
| 477 |
try:
|
| 478 |
+
# Get new start and end positions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
try:
|
| 480 |
new_start = int(self.view.line_edit_start_location.text())
|
| 481 |
new_end = int(self.view.line_edit_stop_location.text())
|
|
|
|
| 512 |
)
|
| 513 |
return
|
| 514 |
|
| 515 |
+
# Get current gene/position
|
| 516 |
+
current_gene = self.view.combo_box_gene.currentText()
|
| 517 |
+
|
| 518 |
+
# Handle position-based search
|
| 519 |
if "chromosome" in current_gene and "start:" in current_gene:
|
| 520 |
# For position-based searches
|
| 521 |
try:
|
|
|
|
| 523 |
# Get full chromosome identifier instead of just the number
|
| 524 |
chrom = parts[0].split('chromosome')[1].strip() # This will now keep the full identifier
|
| 525 |
|
| 526 |
+
# Get sequence for new range - subtract 1 from start for 0-based indexing
|
| 527 |
+
sequence = self.model._get_sequence_for_position(chrom, new_start - 1, new_end)
|
| 528 |
|
| 529 |
if sequence:
|
| 530 |
+
# Update DNA viewer with sequence
|
| 531 |
+
self.view.dna_feature_viewer.set_data(sequence, [], new_start - 1)
|
| 532 |
+
|
| 533 |
+
# Update the line edits with new positions (keep display as 1-based)
|
| 534 |
self.view.line_edit_start_location.setText(str(new_start))
|
| 535 |
self.view.line_edit_stop_location.setText(str(new_end))
|
| 536 |
else:
|
|
|
|
| 539 |
except Exception as e:
|
| 540 |
QMessageBox.warning(
|
| 541 |
self.view,
|
| 542 |
+
"Sequence Error",
|
| 543 |
+
f"Could not get sequence for range {new_start}-{new_end} in chromosome {chrom}"
|
| 544 |
)
|
|
|
|
| 545 |
else:
|
| 546 |
+
# For gene-based searches
|
| 547 |
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
|
|
|
| 548 |
|
| 549 |
+
# Get gene data to get chromosome information
|
| 550 |
+
gene_data = self.model.get_gene_data(locus_tag)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
|
| 552 |
+
if gene_data and 'info' in gene_data:
|
| 553 |
+
try:
|
| 554 |
+
# Get chromosome from gene data
|
| 555 |
+
chrom = gene_data['info']['chromosome']
|
| 556 |
+
|
| 557 |
+
# Get sequence directly using _get_sequence_for_position
|
| 558 |
+
sequence = self.model._get_sequence_for_position(chrom, new_start - 1, new_end)
|
| 559 |
+
|
| 560 |
+
if sequence:
|
| 561 |
+
# Update line edits (keep display as 1-based)
|
| 562 |
+
self.view.line_edit_start_location.setText(str(new_start))
|
| 563 |
+
self.view.line_edit_stop_location.setText(str(new_end))
|
| 564 |
+
|
| 565 |
+
# Get features for this gene
|
| 566 |
+
features = self.model.get_features_for_gene(locus_tag)
|
| 567 |
+
|
| 568 |
+
# Update DNA viewer with new sequence
|
| 569 |
+
self.view.dna_feature_viewer.set_data(sequence, features, new_start - 1)
|
| 570 |
+
|
| 571 |
+
# Update guides display for new range
|
| 572 |
+
gene_guides = [g for g in self.model.guides
|
| 573 |
+
if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower() and
|
| 574 |
+
new_start <= int(g['location'].split('-')[0]) <= new_end]
|
| 575 |
+
self.view.display_guides_in_table(gene_guides)
|
| 576 |
+
else:
|
| 577 |
+
raise ValueError("Could not get sequence for the specified range")
|
| 578 |
+
|
| 579 |
+
except ValueError as ve:
|
| 580 |
+
QMessageBox.warning(
|
| 581 |
+
self.view,
|
| 582 |
+
"Range Error",
|
| 583 |
+
f"Invalid range: {str(ve)}"
|
| 584 |
+
)
|
| 585 |
else:
|
| 586 |
QMessageBox.warning(
|
| 587 |
self.view,
|
| 588 |
+
"Gene Error",
|
| 589 |
+
f"Could not get sequence data for gene {locus_tag}"
|
| 590 |
)
|
| 591 |
+
|
|
|
|
| 592 |
except Exception as e:
|
| 593 |
+
self.logger.error(f"Error changing indices: {str(e)}")
|
| 594 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 595 |
+
show_error(self.settings, "Error", f"Could not change sequence range: {str(e)}")
|
| 596 |
|
| 597 |
def reset_location(self):
|
| 598 |
"""Reset gene viewer to the original sequence and location"""
|
|
|
|
| 612 |
# Get sequence directly using model's method
|
| 613 |
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 614 |
if sequence:
|
| 615 |
+
# Update DNA viewer with sequence
|
| 616 |
+
self.view.dna_feature_viewer.set_data(sequence, [], start)
|
| 617 |
|
| 618 |
+
# Update location fields - add 1 to start for display
|
| 619 |
self.view.line_edit_start_location.setText(str(start + 1))
|
| 620 |
self.view.line_edit_stop_location.setText(str(end))
|
| 621 |
+
|
| 622 |
+
# Update guides display
|
| 623 |
+
position_guides = [g for g in self.model.guides
|
| 624 |
+
if start <= int(g['location'].split('-')[0]) <= end]
|
| 625 |
+
self.view.display_guides_in_table(position_guides)
|
| 626 |
else:
|
| 627 |
raise ValueError("Could not get sequence for position")
|
| 628 |
|
|
|
|
| 635 |
)
|
| 636 |
return
|
| 637 |
else:
|
| 638 |
+
# For gene-based searches
|
| 639 |
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
|
|
|
| 640 |
|
| 641 |
+
# Get original gene sequence
|
| 642 |
+
sequence_data = self.model.get_gene_sequence(locus_tag)
|
| 643 |
if sequence_data:
|
| 644 |
+
# Get features for this gene
|
| 645 |
+
features = self.model.get_features_for_gene(locus_tag)
|
| 646 |
|
| 647 |
+
# Update DNA viewer with sequence and features
|
| 648 |
+
self.view.dna_feature_viewer.set_data(
|
| 649 |
+
sequence_data['sequence'],
|
| 650 |
+
features,
|
| 651 |
+
sequence_data['start']
|
| 652 |
+
)
|
| 653 |
+
|
| 654 |
+
# Update location fields - add 1 to start for display
|
| 655 |
self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
|
| 656 |
self.view.line_edit_stop_location.setText(str(sequence_data['end']))
|
| 657 |
+
|
| 658 |
+
# Update guides display
|
| 659 |
+
gene_guides = [g for g in self.model.guides
|
| 660 |
+
if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
|
| 661 |
+
self.view.display_guides_in_table(gene_guides)
|
| 662 |
else:
|
|
|
|
| 663 |
QMessageBox.warning(
|
| 664 |
self.view,
|
| 665 |
+
"Gene Error",
|
| 666 |
+
f"Could not get sequence data for gene {locus_tag}"
|
| 667 |
)
|
| 668 |
+
|
|
|
|
| 669 |
except Exception as e:
|
| 670 |
self.logger.error(f"Error in reset_location: {str(e)}")
|
| 671 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 672 |
+
show_error(self.settings, "Reset Error", str(e))
|
| 673 |
|
| 674 |
def select_all(self, state):
|
| 675 |
try:
|
|
|
|
| 699 |
# Get current guides from model
|
| 700 |
self.model.load_guides(self.selected_targets, self.organism, self.endonuclease)
|
| 701 |
guides = self.model.get_guides()
|
| 702 |
+
|
| 703 |
+
# Clear any existing highlights and cursor
|
| 704 |
+
self._clear_viewer_state()
|
| 705 |
+
|
| 706 |
self.view.display_guides_in_table(guides)
|
| 707 |
except Exception as e:
|
| 708 |
show_error(self.settings, "Error refreshing guides display", str(e))
|
|
|
|
| 719 |
QApplication.processEvents()
|
| 720 |
|
| 721 |
try:
|
| 722 |
+
# Clear any existing highlights and cursor
|
| 723 |
+
self._clear_viewer_state()
|
| 724 |
+
|
| 725 |
# Load data in chunks
|
| 726 |
loading_dialog.set_message("Loading sequence data...", 30)
|
| 727 |
QApplication.processEvents()
|
|
|
|
| 737 |
self.view.line_edit_start_location.setText(str(start))
|
| 738 |
self.view.line_edit_stop_location.setText(str(end))
|
| 739 |
|
| 740 |
+
# Get sequence directly for position-based search
|
| 741 |
+
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 742 |
+
if sequence:
|
| 743 |
+
# Update DNA viewer with sequence
|
| 744 |
+
self.view.dna_feature_viewer.set_data(sequence, [], start)
|
| 745 |
+
self.logger.debug(f"Updated gene viewer with sequence of length {len(sequence)}")
|
| 746 |
+
else:
|
| 747 |
+
self.logger.error(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
|
| 748 |
+
|
| 749 |
+
# Filter guides for this position
|
| 750 |
+
position_guides = [g for g in self.model.guides if g.get('feature_id') == selected_text]
|
| 751 |
self.view.display_guides_in_table(position_guides)
|
| 752 |
|
| 753 |
else:
|
| 754 |
+
# Regular gene-based search
|
| 755 |
locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
|
| 756 |
+
gene_data = self.model.get_gene_data(locus_tag)
|
| 757 |
|
| 758 |
+
if gene_data and 'sequence' in gene_data and 'info' in gene_data:
|
| 759 |
+
self.view.line_edit_start_location.setText(str(gene_data['info']['start'] + 1))
|
| 760 |
+
self.view.line_edit_stop_location.setText(str(gene_data['info']['end']))
|
| 761 |
|
| 762 |
loading_dialog.set_message("Updating display...", 80)
|
| 763 |
QApplication.processEvents()
|
| 764 |
|
| 765 |
+
# Get features and update viewer in a single call
|
| 766 |
+
features = self.model.get_features_for_gene(locus_tag)
|
| 767 |
+
self.view.dna_feature_viewer.set_data(gene_data['sequence'], features, gene_data['info']['start'])
|
| 768 |
+
|
| 769 |
+
# Filter guides for this gene
|
| 770 |
gene_guides = [g for g in self.model.guides
|
| 771 |
+
if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
|
| 772 |
self.view.display_guides_in_table(gene_guides)
|
| 773 |
+
else:
|
| 774 |
+
self.logger.warning(f"No valid gene data found for locus tag: {locus_tag}")
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
finally:
|
| 777 |
loading_dialog.close()
|
|
|
|
| 822 |
print("Negative strand")
|
| 823 |
# Convert sequence to complement for negative strand search
|
| 824 |
print(f"Sequence: {sequence_upper}")
|
| 825 |
+
complement_sequence = ''.join({'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G',
|
| 826 |
+
'K': 'M', 'Y': 'R', 'R': 'Y', 'M': 'K',
|
| 827 |
+
'S': 'S'}[base] for base in sequence_upper)
|
| 828 |
print(f"Complement sequence: {complement_sequence}")
|
| 829 |
target_sequence = guide_sequence.upper()
|
| 830 |
print(f"Target sequence: {target_sequence}")
|
| 831 |
+
target_sequence = target_sequence[::-1] # Reverse the sequence
|
| 832 |
print(f"Reversed target sequence: {target_sequence}")
|
| 833 |
pos = complement_sequence.find(target_sequence)
|
| 834 |
print(f"Position: {pos}")
|
| 835 |
+
|
| 836 |
if pos != -1:
|
| 837 |
color = QColor(255, 0, 0, 100) # Red for negative strand
|
| 838 |
self.logger.debug(f"Found negative strand sequence at position {pos}")
|
| 839 |
|
| 840 |
+
# For negative strand, use the position directly but indicate strand
|
| 841 |
self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
|
| 842 |
pos,
|
| 843 |
pos + len(guide_sequence) - 1,
|
|
|
|
| 1123 |
chrom = parts[0].split('chromosome')[1].strip()
|
| 1124 |
start = int(parts[1].split('start:')[1].strip())
|
| 1125 |
end = int(parts[2].split('end:')[1].strip())
|
| 1126 |
+
|
| 1127 |
+
self.logger.debug(f"Getting sequence for position: {chrom}:{start}-{end}")
|
| 1128 |
sequence = self.model._get_sequence_for_position(chrom, start, end)
|
| 1129 |
|
| 1130 |
if sequence:
|
| 1131 |
# Get features for this region
|
| 1132 |
features = self.model.get_features_for_region(chrom, start, end)
|
| 1133 |
+
self.logger.debug(f"Got sequence of length {len(sequence)} and {len(features)} features")
|
| 1134 |
+
|
| 1135 |
+
# Verify DNA viewer exists
|
| 1136 |
+
if not hasattr(self.view, 'dna_feature_viewer'):
|
| 1137 |
+
self.logger.error("DNA viewer not initialized!")
|
| 1138 |
+
return
|
| 1139 |
+
|
| 1140 |
+
# Update DNA viewer directly
|
| 1141 |
+
self.view.dna_feature_viewer.set_data(sequence, features, start)
|
| 1142 |
+
self.logger.debug("Updated DNA viewer with sequence data")
|
| 1143 |
+
else:
|
| 1144 |
+
self.logger.error("Failed to get sequence for position")
|
| 1145 |
else:
|
| 1146 |
# Regular gene-based search
|
| 1147 |
locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
|
|
|
|
| 1151 |
if sequence_data and 'sequence' in sequence_data:
|
| 1152 |
# Get features for this gene
|
| 1153 |
features = self.model.get_features_for_gene(locus_tag)
|
| 1154 |
+
sequence = sequence_data['sequence']
|
| 1155 |
+
start_pos = sequence_data['start']
|
| 1156 |
+
|
| 1157 |
+
self.logger.debug(f"Got sequence of length {len(sequence)}")
|
| 1158 |
+
self.logger.debug(f"First 50 chars: {sequence[:50]}")
|
| 1159 |
+
self.logger.debug(f"Start position: {start_pos}")
|
| 1160 |
+
self.logger.debug(f"Number of features: {len(features)}")
|
| 1161 |
+
|
| 1162 |
+
# Verify DNA viewer exists
|
| 1163 |
+
if not hasattr(self.view, 'dna_feature_viewer'):
|
| 1164 |
+
self.logger.error("DNA viewer not initialized!")
|
| 1165 |
+
return
|
| 1166 |
+
|
| 1167 |
+
# Update DNA viewer directly
|
| 1168 |
+
self.view.dna_feature_viewer.set_data(sequence, features, start_pos)
|
| 1169 |
+
self.logger.debug("Updated DNA viewer with sequence data")
|
| 1170 |
else:
|
| 1171 |
self.logger.warning("No sequence data available")
|
| 1172 |
|
|
@@ -2,42 +2,72 @@ import sys
|
|
| 2 |
import os
|
| 3 |
import platform
|
| 4 |
from PyQt6.QtWidgets import QApplication
|
| 5 |
-
from PyQt6.QtCore import Qt
|
| 6 |
from models.GlobalSettings import GlobalSettings
|
| 7 |
from utils.ui import show_error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def get_app_directory():
|
| 10 |
"""Determine the application root directory based on whether we're frozen or not"""
|
| 11 |
if hasattr(sys, 'frozen'):
|
| 12 |
if platform.system() == 'Darwin': # macOS
|
| 13 |
-
# Get the path to the executable inside the .app bundle
|
| 14 |
bundle_dir = os.path.abspath(os.path.dirname(sys.executable))
|
| 15 |
-
# Navigate up to Contents directory and set Resources as app_dir
|
| 16 |
return os.path.join(os.path.dirname(os.path.dirname(bundle_dir)), 'Contents', 'Resources')
|
| 17 |
else:
|
| 18 |
-
# For other platforms when frozen
|
| 19 |
return os.path.dirname(sys.executable)
|
| 20 |
else:
|
| 21 |
-
# Development environment - go up one level from src directory
|
| 22 |
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 23 |
|
| 24 |
def main():
|
| 25 |
-
RESTART_CODE = 1000
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
while True:
|
| 28 |
app = QApplication(sys.argv)
|
| 29 |
app.setOrganizationName("TrinhLab-UTK")
|
| 30 |
app.setApplicationName("CASPER")
|
| 31 |
|
|
|
|
| 32 |
if hasattr(Qt.ApplicationAttribute, 'AA_UseHighDpiPixmaps'):
|
| 33 |
app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
|
|
|
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
# Get the application directory
|
| 36 |
app_dir_path = get_app_directory()
|
| 37 |
|
| 38 |
try:
|
| 39 |
global_settings = GlobalSettings(app_dir_path)
|
| 40 |
|
|
|
|
| 41 |
from controllers.MainWindowController import MainWindowController
|
| 42 |
main_window_controller = MainWindowController(global_settings)
|
| 43 |
global_settings.set_main_window(main_window_controller)
|
|
|
|
| 2 |
import os
|
| 3 |
import platform
|
| 4 |
from PyQt6.QtWidgets import QApplication
|
| 5 |
+
from PyQt6.QtCore import Qt, QCoreApplication
|
| 6 |
from models.GlobalSettings import GlobalSettings
|
| 7 |
from utils.ui import show_error
|
| 8 |
+
import importlib
|
| 9 |
+
import concurrent.futures
|
| 10 |
+
|
| 11 |
+
def preload_modules():
|
| 12 |
+
"""Preload commonly used modules in parallel"""
|
| 13 |
+
modules_to_load = [
|
| 14 |
+
'PyQt6.QtWidgets',
|
| 15 |
+
'PyQt6.QtCore',
|
| 16 |
+
'PyQt6.QtGui',
|
| 17 |
+
'controllers.MainWindowController',
|
| 18 |
+
'views.MainWindowView',
|
| 19 |
+
'models.MainWindowModel',
|
| 20 |
+
'models.DatabaseManager',
|
| 21 |
+
'models.ConfigManager'
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
def import_module(module_name):
|
| 25 |
+
try:
|
| 26 |
+
importlib.import_module(module_name)
|
| 27 |
+
return True, module_name
|
| 28 |
+
except Exception as e:
|
| 29 |
+
return False, f"Failed to load {module_name}: {str(e)}"
|
| 30 |
+
|
| 31 |
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
| 32 |
+
executor.map(import_module, modules_to_load)
|
| 33 |
|
| 34 |
def get_app_directory():
|
| 35 |
"""Determine the application root directory based on whether we're frozen or not"""
|
| 36 |
if hasattr(sys, 'frozen'):
|
| 37 |
if platform.system() == 'Darwin': # macOS
|
|
|
|
| 38 |
bundle_dir = os.path.abspath(os.path.dirname(sys.executable))
|
|
|
|
| 39 |
return os.path.join(os.path.dirname(os.path.dirname(bundle_dir)), 'Contents', 'Resources')
|
| 40 |
else:
|
|
|
|
| 41 |
return os.path.dirname(sys.executable)
|
| 42 |
else:
|
|
|
|
| 43 |
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 44 |
|
| 45 |
def main():
|
| 46 |
+
RESTART_CODE = 1000
|
| 47 |
+
|
| 48 |
+
# Preload modules in parallel
|
| 49 |
+
preload_modules()
|
| 50 |
|
| 51 |
while True:
|
| 52 |
app = QApplication(sys.argv)
|
| 53 |
app.setOrganizationName("TrinhLab-UTK")
|
| 54 |
app.setApplicationName("CASPER")
|
| 55 |
|
| 56 |
+
# Enable high DPI scaling
|
| 57 |
if hasattr(Qt.ApplicationAttribute, 'AA_UseHighDpiPixmaps'):
|
| 58 |
app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
|
| 59 |
+
app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
|
| 60 |
|
| 61 |
+
# Enable Qt's built-in caching mechanisms
|
| 62 |
+
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts)
|
| 63 |
+
|
| 64 |
# Get the application directory
|
| 65 |
app_dir_path = get_app_directory()
|
| 66 |
|
| 67 |
try:
|
| 68 |
global_settings = GlobalSettings(app_dir_path)
|
| 69 |
|
| 70 |
+
# Import here after preloading
|
| 71 |
from controllers.MainWindowController import MainWindowController
|
| 72 |
main_window_controller = MainWindowController(global_settings)
|
| 73 |
global_settings.set_main_window(main_window_controller)
|
|
@@ -36,7 +36,6 @@ class AnnotationParser:
|
|
| 36 |
|
| 37 |
if self.annotation_file_name != file_path:
|
| 38 |
self.annotation_file_name = file_path
|
| 39 |
-
self.logger.debug(f"Set annotation file to: {file_path}")
|
| 40 |
|
| 41 |
# Set index file path
|
| 42 |
self.index_file = f"{file_path}.index"
|
|
@@ -90,10 +89,18 @@ class AnnotationParser:
|
|
| 90 |
|
| 91 |
# Only process features with valid locus tags
|
| 92 |
if locus_tag and locus_tag.lower() != "n/a":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
# Handle joined locations
|
| 94 |
if isinstance(feature.location, Bio.SeqFeature.CompoundLocation):
|
| 95 |
-
if locus_tag == "
|
| 96 |
print(f"Feature location: {feature.location}")
|
|
|
|
| 97 |
# Get all parts of the joined location
|
| 98 |
parts = feature.location.parts
|
| 99 |
# Find min start and max end across all parts
|
|
@@ -121,10 +128,6 @@ class AnnotationParser:
|
|
| 121 |
strand = '+' if feature.location.strand == 1 else '-'
|
| 122 |
full_location = f"{start}..{end}({strand})"
|
| 123 |
|
| 124 |
-
# Get description first since we might need it for the name
|
| 125 |
-
description = feature.qualifiers.get('product',
|
| 126 |
-
feature.qualifiers.get('note', ['N/A']))[0]
|
| 127 |
-
|
| 128 |
# Get gene name, use description if gene name is N/A
|
| 129 |
gene_name = feature.qualifiers.get('gene', ['N/A'])[0]
|
| 130 |
if gene_name == 'N/A':
|
|
@@ -142,35 +145,40 @@ class AnnotationParser:
|
|
| 142 |
'end': end
|
| 143 |
}
|
| 144 |
|
| 145 |
-
# Update index based on priority
|
| 146 |
if locus_tag in index_data['locus_tags']:
|
| 147 |
existing_entry = index_data['locus_tags'][locus_tag]
|
| 148 |
existing_priority = feature_priority[existing_entry['feature_type']]
|
| 149 |
current_priority = feature_priority[feature.type]
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
if current_priority >= existing_priority:
|
| 152 |
-
# Keep the RNA/higher priority feature type
|
| 153 |
-
merged_entry = existing_entry.copy()
|
| 154 |
merged_entry['feature_type'] = feature.type
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
| 166 |
merged_entry.update({
|
| 167 |
'location': feature_entry['location'],
|
| 168 |
'full_location': feature_entry['full_location'],
|
| 169 |
'start': feature_entry['start'],
|
| 170 |
'end': feature_entry['end']
|
| 171 |
})
|
| 172 |
-
|
| 173 |
-
|
| 174 |
else:
|
| 175 |
# New entry
|
| 176 |
index_data['locus_tags'][locus_tag] = feature_entry
|
|
@@ -219,17 +227,17 @@ class AnnotationParser:
|
|
| 219 |
self.logger.debug(f"Searching in annotation file: {self.annotation_file_name}")
|
| 220 |
results_list = []
|
| 221 |
|
| 222 |
-
# Convert queries to lowercase set for faster lookup
|
| 223 |
queries = {q.lower() for q in queries}
|
| 224 |
-
self.logger.debug(f"Search queries: {queries}")
|
| 225 |
|
| 226 |
# Search through index
|
| 227 |
if hasattr(self, '_index') and 'locus_tags' in self._index:
|
| 228 |
for locus_tag, feature_entry in self._index['locus_tags'].items():
|
|
|
|
| 229 |
searchable_text = ' '.join([
|
| 230 |
feature_entry.get('gene_name', '').lower(),
|
| 231 |
locus_tag.lower(),
|
| 232 |
-
feature_entry.get('description', '').lower()
|
|
|
|
| 233 |
])
|
| 234 |
|
| 235 |
# Check if any query matches
|
|
@@ -319,7 +327,6 @@ class AnnotationParser:
|
|
| 319 |
def _get_sequence_for_gene(self, gene_info):
|
| 320 |
"""Get sequence for a gene from the GenBank file"""
|
| 321 |
try:
|
| 322 |
-
self.logger.debug(f"Getting sequence for gene info: {gene_info} in _get_sequence_for_gene")
|
| 323 |
# Parse the GenBank file and find the right record
|
| 324 |
for record in SeqIO.parse(self.annotation_file_name, "genbank"):
|
| 325 |
if record.id == gene_info['chromosome']: # Use full chromosome name
|
|
@@ -330,11 +337,39 @@ class AnnotationParser:
|
|
| 330 |
start = max(0, gene_info['start'] - padding)
|
| 331 |
end = min(len(sequence), gene_info['end'] + padding)
|
| 332 |
padded_sequence = sequence[start:end]
|
| 333 |
-
|
| 334 |
-
self.logger.debug(f"Padded sequence: {padded_sequence}")
|
| 335 |
return padded_sequence
|
| 336 |
return None
|
| 337 |
|
| 338 |
except Exception as e:
|
| 339 |
self.logger.error(f"Error getting sequence for gene: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
return None
|
|
|
|
| 36 |
|
| 37 |
if self.annotation_file_name != file_path:
|
| 38 |
self.annotation_file_name = file_path
|
|
|
|
| 39 |
|
| 40 |
# Set index file path
|
| 41 |
self.index_file = f"{file_path}.index"
|
|
|
|
| 89 |
|
| 90 |
# Only process features with valid locus tags
|
| 91 |
if locus_tag and locus_tag.lower() != "n/a":
|
| 92 |
+
# Get description, use product as fallback
|
| 93 |
+
description = feature.qualifiers.get('description', ['N/A'])[0]
|
| 94 |
+
if description == 'N/A' or not description:
|
| 95 |
+
description = feature.qualifiers.get('product', ['N/A'])[0]
|
| 96 |
+
if locus_tag == "BN896_RS00070":
|
| 97 |
+
print(f"Feature description: {description}")
|
| 98 |
+
|
| 99 |
# Handle joined locations
|
| 100 |
if isinstance(feature.location, Bio.SeqFeature.CompoundLocation):
|
| 101 |
+
if locus_tag == "BN896_RS00070":
|
| 102 |
print(f"Feature location: {feature.location}")
|
| 103 |
+
# print(f"Feature product:" )
|
| 104 |
# Get all parts of the joined location
|
| 105 |
parts = feature.location.parts
|
| 106 |
# Find min start and max end across all parts
|
|
|
|
| 128 |
strand = '+' if feature.location.strand == 1 else '-'
|
| 129 |
full_location = f"{start}..{end}({strand})"
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
# Get gene name, use description if gene name is N/A
|
| 132 |
gene_name = feature.qualifiers.get('gene', ['N/A'])[0]
|
| 133 |
if gene_name == 'N/A':
|
|
|
|
| 145 |
'end': end
|
| 146 |
}
|
| 147 |
|
| 148 |
+
# Update index based on modified priority logic
|
| 149 |
if locus_tag in index_data['locus_tags']:
|
| 150 |
existing_entry = index_data['locus_tags'][locus_tag]
|
| 151 |
existing_priority = feature_priority[existing_entry['feature_type']]
|
| 152 |
current_priority = feature_priority[feature.type]
|
| 153 |
|
| 154 |
+
# Always create a merged entry
|
| 155 |
+
merged_entry = existing_entry.copy()
|
| 156 |
+
|
| 157 |
+
# Update feature type only if priority is higher
|
| 158 |
if current_priority >= existing_priority:
|
|
|
|
|
|
|
| 159 |
merged_entry['feature_type'] = feature.type
|
| 160 |
+
|
| 161 |
+
# Always update description if new one is not N/A
|
| 162 |
+
if feature_entry['description'] != 'N/A':
|
| 163 |
+
merged_entry['description'] = feature_entry['description']
|
| 164 |
+
# If gene name is N/A, use the new description
|
| 165 |
+
if merged_entry['gene_name'] == 'N/A':
|
| 166 |
+
merged_entry['gene_name'] = feature_entry['description']
|
| 167 |
+
|
| 168 |
+
# Update other fields if they're not 'N/A'
|
| 169 |
+
if feature_entry['gene_name'] != 'N/A':
|
| 170 |
+
merged_entry['gene_name'] = feature_entry['gene_name']
|
| 171 |
+
|
| 172 |
+
# Always update location information if priority is higher
|
| 173 |
+
if current_priority >= existing_priority:
|
| 174 |
merged_entry.update({
|
| 175 |
'location': feature_entry['location'],
|
| 176 |
'full_location': feature_entry['full_location'],
|
| 177 |
'start': feature_entry['start'],
|
| 178 |
'end': feature_entry['end']
|
| 179 |
})
|
| 180 |
+
|
| 181 |
+
index_data['locus_tags'][locus_tag] = merged_entry
|
| 182 |
else:
|
| 183 |
# New entry
|
| 184 |
index_data['locus_tags'][locus_tag] = feature_entry
|
|
|
|
| 227 |
self.logger.debug(f"Searching in annotation file: {self.annotation_file_name}")
|
| 228 |
results_list = []
|
| 229 |
|
|
|
|
| 230 |
queries = {q.lower() for q in queries}
|
|
|
|
| 231 |
|
| 232 |
# Search through index
|
| 233 |
if hasattr(self, '_index') and 'locus_tags' in self._index:
|
| 234 |
for locus_tag, feature_entry in self._index['locus_tags'].items():
|
| 235 |
+
# Create searchable text including feature type
|
| 236 |
searchable_text = ' '.join([
|
| 237 |
feature_entry.get('gene_name', '').lower(),
|
| 238 |
locus_tag.lower(),
|
| 239 |
+
feature_entry.get('description', '').lower(),
|
| 240 |
+
feature_entry.get('feature_type', '').lower() # Add feature type to searchable text
|
| 241 |
])
|
| 242 |
|
| 243 |
# Check if any query matches
|
|
|
|
| 327 |
def _get_sequence_for_gene(self, gene_info):
|
| 328 |
"""Get sequence for a gene from the GenBank file"""
|
| 329 |
try:
|
|
|
|
| 330 |
# Parse the GenBank file and find the right record
|
| 331 |
for record in SeqIO.parse(self.annotation_file_name, "genbank"):
|
| 332 |
if record.id == gene_info['chromosome']: # Use full chromosome name
|
|
|
|
| 337 |
start = max(0, gene_info['start'] - padding)
|
| 338 |
end = min(len(sequence), gene_info['end'] + padding)
|
| 339 |
padded_sequence = sequence[start:end]
|
|
|
|
|
|
|
| 340 |
return padded_sequence
|
| 341 |
return None
|
| 342 |
|
| 343 |
except Exception as e:
|
| 344 |
self.logger.error(f"Error getting sequence for gene: {str(e)}")
|
| 345 |
+
return None
|
| 346 |
+
|
| 347 |
+
def _get_sequence_for_position(self, chrom, start, end):
|
| 348 |
+
"""Get sequence for a specific position range from the GenBank file
|
| 349 |
+
|
| 350 |
+
Args:
|
| 351 |
+
chrom (str): Chromosome identifier
|
| 352 |
+
start (int): Start position (0-based)
|
| 353 |
+
end (int): End position
|
| 354 |
+
|
| 355 |
+
Returns:
|
| 356 |
+
str: The sequence for the specified range with padding, or None if not found
|
| 357 |
+
"""
|
| 358 |
+
try:
|
| 359 |
+
self.logger.debug(f"Getting sequence for position {chrom}:{start}-{end}")
|
| 360 |
+
# Parse the GenBank file and find the right record
|
| 361 |
+
for record in SeqIO.parse(self.annotation_file_name, "genbank"):
|
| 362 |
+
if record.id == chrom: # Use full chromosome name
|
| 363 |
+
sequence = str(record.seq)
|
| 364 |
+
|
| 365 |
+
# Get sequence with padding
|
| 366 |
+
padding = 30
|
| 367 |
+
padded_start = max(0, start - padding)
|
| 368 |
+
padded_end = min(len(sequence), end + padding)
|
| 369 |
+
padded_sequence = sequence[padded_start:padded_end]
|
| 370 |
+
return padded_sequence
|
| 371 |
+
return None
|
| 372 |
+
|
| 373 |
+
except Exception as e:
|
| 374 |
+
self.logger.error(f"Error getting sequence for position: {str(e)}")
|
| 375 |
return None
|
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from models.AnnotationParser import AnnotationParser
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
class BaseModel:
|
| 5 |
+
def __init__(self, global_settings):
|
| 6 |
+
self.global_settings = global_settings
|
| 7 |
+
self.logger = global_settings.get_logger()
|
| 8 |
+
|
| 9 |
+
# Initialize the annotation parser
|
| 10 |
+
self._initialize_annotation_parser()
|
| 11 |
+
|
| 12 |
+
# Connect to annotation file changes
|
| 13 |
+
self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
|
| 14 |
+
|
| 15 |
+
def _initialize_annotation_parser(self) -> None:
|
| 16 |
+
"""Initialize or get existing annotation parser from global settings"""
|
| 17 |
+
try:
|
| 18 |
+
# Try to get existing parser from global settings
|
| 19 |
+
if not hasattr(self.global_settings, 'annotation_parser'):
|
| 20 |
+
# Create new parser if none exists
|
| 21 |
+
annotation_file = self.global_settings.get_current_annotation_file()
|
| 22 |
+
annotation_path = os.path.join(
|
| 23 |
+
self.global_settings.get_db_path(),
|
| 24 |
+
'GBFF',
|
| 25 |
+
annotation_file
|
| 26 |
+
)
|
| 27 |
+
self.global_settings.annotation_parser = AnnotationParser(self.global_settings)
|
| 28 |
+
self.global_settings.annotation_parser.set_annotation_file(annotation_path)
|
| 29 |
+
self.logger.debug("Created new annotation parser")
|
| 30 |
+
|
| 31 |
+
self.annotation_parser = self.global_settings.annotation_parser
|
| 32 |
+
self.annotation_path = self.annotation_parser.annotation_file_name
|
| 33 |
+
|
| 34 |
+
except Exception as e:
|
| 35 |
+
self.logger.error(f"Error initializing annotation parser: {str(e)}")
|
| 36 |
+
raise
|
| 37 |
+
|
| 38 |
+
def _on_annotation_file_changed(self, new_annotation_file):
|
| 39 |
+
"""Handle annotation file changes"""
|
| 40 |
+
try:
|
| 41 |
+
self.logger.debug(f"Clearing caches for new annotation file: {new_annotation_file}")
|
| 42 |
+
self._clear_caches()
|
| 43 |
+
self._initialize_annotation_parser()
|
| 44 |
+
except Exception as e:
|
| 45 |
+
self.logger.error(f"Error handling annotation file change: {str(e)}")
|
| 46 |
+
|
| 47 |
+
def _clear_caches(self):
|
| 48 |
+
"""Clear model-specific caches. Override in subclasses."""
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
def cleanup(self):
|
| 52 |
+
"""Cleanup resources. Override in subclasses if needed."""
|
| 53 |
+
try:
|
| 54 |
+
self.global_settings.annotation_file_changed.disconnect(self._on_annotation_file_changed)
|
| 55 |
+
self.logger.debug("Disconnected from annotation file changes")
|
| 56 |
+
self._clear_caches()
|
| 57 |
+
except Exception as e:
|
| 58 |
+
self.logger.error(f"Error in cleanup: {str(e)}")
|
|
@@ -94,24 +94,71 @@ class ConfigManager(QObject):
|
|
| 94 |
self.env_file_created.emit()
|
| 95 |
|
| 96 |
def write_to_env(self, key, value):
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
|
|
|
|
|
|
|
| 101 |
for line in lines:
|
| 102 |
-
if line.startswith(f'{key}='):
|
| 103 |
-
|
|
|
|
| 104 |
else:
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
def get_env_value(self, key, default=None):
|
| 109 |
return os.getenv(key, default)
|
| 110 |
|
| 111 |
def set_env_value(self, key, value):
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
def get_config_value(self, key, default=None):
|
| 117 |
keys = key.split('.')
|
|
|
|
| 94 |
self.env_file_created.emit()
|
| 95 |
|
| 96 |
def write_to_env(self, key, value):
|
| 97 |
+
"""Write or update a key-value pair in the .env file"""
|
| 98 |
+
try:
|
| 99 |
+
# Read all lines
|
| 100 |
+
if os.path.exists(self.env_path):
|
| 101 |
+
with open(self.env_path, 'r') as f:
|
| 102 |
+
lines = f.readlines()
|
| 103 |
+
else:
|
| 104 |
+
lines = []
|
| 105 |
|
| 106 |
+
# Find if key exists
|
| 107 |
+
key_exists = False
|
| 108 |
+
new_lines = []
|
| 109 |
for line in lines:
|
| 110 |
+
if line.strip().startswith(f'{key}='):
|
| 111 |
+
new_lines.append(f'{key}="{value}"\n') # Always use double quotes
|
| 112 |
+
key_exists = True
|
| 113 |
else:
|
| 114 |
+
new_lines.append(line)
|
| 115 |
+
|
| 116 |
+
# If key doesn't exist, append it
|
| 117 |
+
if not key_exists:
|
| 118 |
+
new_lines.append(f'{key}="{value}"\n')
|
| 119 |
+
|
| 120 |
+
# Write back to file
|
| 121 |
+
with open(self.env_path, 'w') as f:
|
| 122 |
+
f.writelines(new_lines)
|
| 123 |
+
|
| 124 |
+
# Update environment variable in memory
|
| 125 |
+
os.environ[key] = value
|
| 126 |
+
|
| 127 |
+
self.logger.info(f"Updated {key}={value} in .env file")
|
| 128 |
+
|
| 129 |
+
# Verify the write
|
| 130 |
+
with open(self.env_path, 'r') as f:
|
| 131 |
+
content = f.read()
|
| 132 |
+
if f'{key}="{value}"' not in content:
|
| 133 |
+
self.logger.error(f"Failed to verify {key}={value} in .env file")
|
| 134 |
+
raise Exception("Failed to verify environment variable update")
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
self.logger.error(f"Error writing to .env file: {str(e)}")
|
| 138 |
+
raise
|
| 139 |
|
| 140 |
def get_env_value(self, key, default=None):
|
| 141 |
return os.getenv(key, default)
|
| 142 |
|
| 143 |
def set_env_value(self, key, value):
|
| 144 |
+
"""Set and save an environment variable"""
|
| 145 |
+
try:
|
| 146 |
+
# Write to .env file first
|
| 147 |
+
self.write_to_env(key, value)
|
| 148 |
+
|
| 149 |
+
# Update runtime environment
|
| 150 |
+
os.environ[key] = value
|
| 151 |
+
|
| 152 |
+
# Verify the update
|
| 153 |
+
if os.getenv(key) != value:
|
| 154 |
+
self.logger.error(f"Failed to verify environment variable update for {key}")
|
| 155 |
+
raise Exception(f"Environment variable verification failed for {key}")
|
| 156 |
+
|
| 157 |
+
self.logger.info(f"Successfully set environment variable: {key}={value}")
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
self.logger.error(f"Error setting environment variable {key}: {str(e)}")
|
| 161 |
+
raise
|
| 162 |
|
| 163 |
def get_config_value(self, key, default=None):
|
| 164 |
keys = key.split('.')
|
|
@@ -5,6 +5,7 @@ from collections import Counter
|
|
| 5 |
import statistics
|
| 6 |
from enum import Enum
|
| 7 |
from typing import Set, Dict, List, Tuple
|
|
|
|
| 8 |
|
| 9 |
class FileChangeType(Enum):
|
| 10 |
CSPR_ADDED = "cspr_added"
|
|
@@ -23,86 +24,175 @@ class DatabaseManager(QObject):
|
|
| 23 |
self.logger = logger
|
| 24 |
self.config_manager = config_manager
|
| 25 |
self.db_path = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
self.file_watcher = QFileSystemWatcher()
|
| 27 |
self.file_watcher.directoryChanged.connect(self._on_directory_changed)
|
|
|
|
|
|
|
| 28 |
self.load_database_path()
|
| 29 |
self._update_watched_directory()
|
| 30 |
|
| 31 |
-
|
| 32 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
def load_database_path(self):
|
| 35 |
-
"""Load the database path from .env file
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
def validate_db_path(self, path):
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def save_db_path(self, path):
|
| 62 |
"""Set and save the database path."""
|
| 63 |
-
if
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
if not is_valid:
|
| 73 |
-
self.logger.warning(f"Invalid database path: {path}")
|
| 74 |
-
self.db_validation_changed.emit(False, message)
|
| 75 |
-
self.db_path = path
|
| 76 |
-
self.config_manager.set_env_value('CSPR_DB', path)
|
| 77 |
-
self._update_watched_directory()
|
| 78 |
-
return False, message
|
| 79 |
-
|
| 80 |
-
# Set the db_path attribute
|
| 81 |
-
self.db_path = path
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
try:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
except Exception as e:
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
def get_db_path(self):
|
| 96 |
return self.db_path
|
| 97 |
|
| 98 |
def ensure_db_path_exists(self):
|
| 99 |
"""Ensure that the database path exists, creating it if necessary."""
|
| 100 |
-
|
|
|
|
| 101 |
try:
|
| 102 |
-
os.makedirs(
|
| 103 |
-
self.logger.info(f"Created database directory: {
|
| 104 |
except Exception as e:
|
| 105 |
-
self.logger.error(f"Failed to create database directory: {
|
| 106 |
raise
|
| 107 |
|
| 108 |
def get_default_database_path(self):
|
|
@@ -190,24 +280,36 @@ class DatabaseManager(QObject):
|
|
| 190 |
try:
|
| 191 |
self.logger.debug(f"Detected change in directory: {path}")
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
# Re-validate the path
|
| 194 |
is_valid, message = self.validate_db_path(self.db_path)
|
| 195 |
|
| 196 |
# Detect specific changes
|
| 197 |
changes = self._detect_file_changes()
|
| 198 |
|
| 199 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
self.db_validation_changed.emit(is_valid, message)
|
|
|
|
| 201 |
|
| 202 |
-
|
|
|
|
| 203 |
self.logger.debug(f"Detected file changes: {changes}")
|
| 204 |
self.db_files_changed.emit(changes)
|
| 205 |
-
|
| 206 |
-
# Always emit combined state signal
|
| 207 |
-
self.db_state_changed.emit(is_valid, message, changes or {})
|
| 208 |
|
| 209 |
-
self.logger.info(f"Database state updated - Valid: {is_valid}, Changes: {changes}")
|
| 210 |
-
|
| 211 |
except Exception as e:
|
| 212 |
self.logger.error(f"Error handling directory change: {str(e)}")
|
| 213 |
|
|
@@ -229,23 +331,24 @@ class DatabaseManager(QObject):
|
|
| 229 |
if f.endswith('.gbff')]
|
| 230 |
|
| 231 |
def check_db_state(self):
|
| 232 |
-
"""
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
is_valid, message = self.validate_db_path(self.db_path)
|
| 239 |
-
|
| 240 |
-
# Detect any changes since last check
|
| 241 |
-
changes = self._detect_file_changes()
|
| 242 |
-
|
| 243 |
-
# Emit signals
|
| 244 |
-
self.db_validation_changed.emit(is_valid, message)
|
| 245 |
-
if changes:
|
| 246 |
-
self.db_files_changed.emit(changes)
|
| 247 |
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
def get_organisms_and_endos(self):
|
| 251 |
"""Get mapping of organisms to their endonucleases and files"""
|
|
@@ -510,3 +613,105 @@ class DatabaseManager(QObject):
|
|
| 510 |
except Exception as e:
|
| 511 |
self.logger.error(f"Error calculating statistics: {str(e)}")
|
| 512 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import statistics
|
| 6 |
from enum import Enum
|
| 7 |
from typing import Set, Dict, List, Tuple
|
| 8 |
+
import glob
|
| 9 |
|
| 10 |
class FileChangeType(Enum):
|
| 11 |
CSPR_ADDED = "cspr_added"
|
|
|
|
| 24 |
self.logger = logger
|
| 25 |
self.config_manager = config_manager
|
| 26 |
self.db_path = None
|
| 27 |
+
self.pending_db_path = None
|
| 28 |
+
self.is_changing_directory = False
|
| 29 |
+
self._is_validating = False # Add flag to prevent recursion
|
| 30 |
+
|
| 31 |
+
# Initialize last known states before file watcher
|
| 32 |
+
self._last_cspr_files = set()
|
| 33 |
+
self._last_gbff_files = set()
|
| 34 |
+
self._last_files = {} # Track files in each watched directory
|
| 35 |
+
|
| 36 |
+
# Initialize file watcher
|
| 37 |
self.file_watcher = QFileSystemWatcher()
|
| 38 |
self.file_watcher.directoryChanged.connect(self._on_directory_changed)
|
| 39 |
+
|
| 40 |
+
# Load database path and update states
|
| 41 |
self.load_database_path()
|
| 42 |
self._update_watched_directory()
|
| 43 |
|
| 44 |
+
# Update last known states after path is loaded
|
| 45 |
+
self._last_cspr_files = set(self._get_cspr_files())
|
| 46 |
+
self._last_gbff_files = set(self._get_gbff_files())
|
| 47 |
+
if self.db_path:
|
| 48 |
+
self._last_files[self.db_path] = set(os.listdir(self.db_path))
|
| 49 |
+
gbff_path = os.path.join(self.db_path, 'GBFF')
|
| 50 |
+
if os.path.exists(gbff_path):
|
| 51 |
+
self._last_files[gbff_path] = set(os.listdir(gbff_path))
|
| 52 |
|
| 53 |
def load_database_path(self):
|
| 54 |
+
"""Load the database path from .env file."""
|
| 55 |
+
try:
|
| 56 |
+
db_path = self.config_manager.get_env_value('CSPR_DB', '')
|
| 57 |
+
# Remove both single and double quotes if present
|
| 58 |
+
db_path = db_path.strip("'\"")
|
| 59 |
+
|
| 60 |
+
# Only set default if no path exists at all
|
| 61 |
+
if not db_path and self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE':
|
| 62 |
+
db_path = self.get_default_database_path()
|
| 63 |
+
self.save_db_path(db_path)
|
| 64 |
+
|
| 65 |
+
self.db_path = db_path
|
| 66 |
+
self.logger.debug(f"Database path loaded from .env: {self.db_path}")
|
| 67 |
+
return self.db_path
|
| 68 |
+
|
| 69 |
+
except Exception as e:
|
| 70 |
+
self.logger.error(f"Error loading database path: {str(e)}")
|
| 71 |
+
return self.get_default_database_path()
|
| 72 |
|
| 73 |
def validate_db_path(self, path):
|
| 74 |
+
"""
|
| 75 |
+
Validate the database path without modifying it
|
| 76 |
+
Returns (is_valid, message)
|
| 77 |
+
"""
|
| 78 |
+
try:
|
| 79 |
+
if not path:
|
| 80 |
+
return False, "No directory selected"
|
| 81 |
+
|
| 82 |
+
if not os.path.exists(path):
|
| 83 |
+
return False, "The selected directory does not exist."
|
| 84 |
|
| 85 |
+
# Check for CSPR files
|
| 86 |
+
cspr_files = glob.glob(os.path.join(path, "*.cspr"))
|
| 87 |
+
if not cspr_files:
|
| 88 |
+
return False, "No CSPR files found"
|
| 89 |
|
| 90 |
+
return True, "Valid database directory"
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
self.logger.error(f"Error validating database path: {str(e)}")
|
| 94 |
+
return False, str(e)
|
| 95 |
|
| 96 |
def save_db_path(self, path):
|
| 97 |
"""Set and save the database path."""
|
| 98 |
+
if self._is_validating: # Prevent recursive validation
|
| 99 |
+
return True, "Operation in progress"
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
self._is_validating = True
|
| 103 |
+
|
| 104 |
+
if not path:
|
| 105 |
+
self.logger.warning("Attempting to save an empty database path")
|
| 106 |
+
return False, "Empty database path is not allowed."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
+
path = str(path).strip("'\"")
|
| 109 |
+
is_new_genome = self._is_new_genome_context()
|
| 110 |
+
|
| 111 |
+
# Validate the database path
|
| 112 |
+
is_valid, message = self.validate_db_path(path)
|
| 113 |
+
|
| 114 |
+
# Log the current state
|
| 115 |
+
self.logger.debug(f"Saving DB path - Current state: db_path={self.db_path}, pending={self.pending_db_path}, "
|
| 116 |
+
f"is_changing={self.is_changing_directory}, is_new_genome={is_new_genome}")
|
| 117 |
+
|
| 118 |
+
# If we're in new genome context or path change context, store as pending path
|
| 119 |
+
if is_new_genome or is_valid:
|
| 120 |
+
self.logger.debug(f"Storing pending database path: {path}")
|
| 121 |
+
self.pending_db_path = path
|
| 122 |
+
self.is_changing_directory = True
|
| 123 |
+
|
| 124 |
+
# Create directory if needed
|
| 125 |
+
if not os.path.exists(path):
|
| 126 |
+
try:
|
| 127 |
+
os.makedirs(path)
|
| 128 |
+
self.logger.info(f"Created pending directory: {path}")
|
| 129 |
+
except Exception as e:
|
| 130 |
+
self.logger.error(f"Error creating pending directory: {str(e)}")
|
| 131 |
+
|
| 132 |
+
# If the path is valid, finalize the change immediately
|
| 133 |
+
if is_valid:
|
| 134 |
+
success, finalize_message = self.finalize_directory_change()
|
| 135 |
+
if not success:
|
| 136 |
+
self.logger.error(f"Failed to finalize directory change: {finalize_message}")
|
| 137 |
+
return False, finalize_message
|
| 138 |
+
return True, finalize_message
|
| 139 |
+
|
| 140 |
+
return True, "Path stored for new genome creation"
|
| 141 |
+
|
| 142 |
+
# For invalid paths
|
| 143 |
+
if not is_valid:
|
| 144 |
+
self.db_validation_changed.emit(False, message)
|
| 145 |
+
return False, message
|
| 146 |
+
|
| 147 |
+
finally:
|
| 148 |
+
self._is_validating = False
|
| 149 |
+
|
| 150 |
+
def _is_new_genome_context(self):
|
| 151 |
+
"""Check if the path change is happening in new genome context"""
|
| 152 |
try:
|
| 153 |
+
import inspect
|
| 154 |
+
stack = inspect.stack()
|
| 155 |
+
|
| 156 |
+
# Check for NCBI context as well
|
| 157 |
+
is_new_genome = any('NewGenome' in frame.filename or 'NCBI' in frame.filename for frame in stack)
|
| 158 |
+
is_path_change = any('MainWindow' in frame.filename and 'change_database_directory' in frame.function
|
| 159 |
+
for frame in stack)
|
| 160 |
+
|
| 161 |
+
# Consider it a new genome context if either:
|
| 162 |
+
# 1. We're in new genome/NCBI context and actively changing directory
|
| 163 |
+
# 2. We're in the path change process
|
| 164 |
+
is_active_change = (is_new_genome and self.is_changing_directory) or is_path_change
|
| 165 |
+
|
| 166 |
+
self.logger.debug(f"Context check - New Genome: {is_new_genome}, Path Change: {is_path_change}, "
|
| 167 |
+
f"Active Change: {is_active_change}, Is Changing Directory: {self.is_changing_directory}")
|
| 168 |
+
|
| 169 |
+
return is_active_change
|
| 170 |
+
|
| 171 |
except Exception as e:
|
| 172 |
+
self.logger.error(f"Error in _is_new_genome_context: {str(e)}")
|
| 173 |
+
return False
|
| 174 |
+
|
| 175 |
+
def get_active_db_path(self):
|
| 176 |
+
"""Get the appropriate database path based on context"""
|
| 177 |
+
if self.pending_db_path and self.is_changing_directory:
|
| 178 |
+
self.logger.debug(f"Using pending database path: {self.pending_db_path}")
|
| 179 |
+
return self.pending_db_path
|
| 180 |
+
|
| 181 |
+
self.logger.debug(f"Using current database path: {self.db_path}")
|
| 182 |
+
return self.db_path
|
| 183 |
|
| 184 |
def get_db_path(self):
|
| 185 |
return self.db_path
|
| 186 |
|
| 187 |
def ensure_db_path_exists(self):
|
| 188 |
"""Ensure that the database path exists, creating it if necessary."""
|
| 189 |
+
path_to_check = self.get_active_db_path() # Use active path instead of db_path
|
| 190 |
+
if not os.path.exists(path_to_check):
|
| 191 |
try:
|
| 192 |
+
os.makedirs(path_to_check)
|
| 193 |
+
self.logger.info(f"Created database directory: {path_to_check}")
|
| 194 |
except Exception as e:
|
| 195 |
+
self.logger.error(f"Failed to create database directory: {path_to_check}. Error: {str(e)}")
|
| 196 |
raise
|
| 197 |
|
| 198 |
def get_default_database_path(self):
|
|
|
|
| 280 |
try:
|
| 281 |
self.logger.debug(f"Detected change in directory: {path}")
|
| 282 |
|
| 283 |
+
# Check if change is just an index file
|
| 284 |
+
changed_files = set(os.listdir(path)) - set(self._last_files.get(path, []))
|
| 285 |
+
if all(f.endswith('.index') for f in changed_files):
|
| 286 |
+
self.logger.debug("Ignoring index file changes")
|
| 287 |
+
# Update last files without triggering refresh
|
| 288 |
+
self._last_files[path] = set(os.listdir(path))
|
| 289 |
+
return
|
| 290 |
+
|
| 291 |
+
# Get current state of CSPR files
|
| 292 |
+
current_cspr_files = set(self._get_cspr_files())
|
| 293 |
+
|
| 294 |
# Re-validate the path
|
| 295 |
is_valid, message = self.validate_db_path(self.db_path)
|
| 296 |
|
| 297 |
# Detect specific changes
|
| 298 |
changes = self._detect_file_changes()
|
| 299 |
|
| 300 |
+
# Update last known state
|
| 301 |
+
self._last_cspr_files = current_cspr_files
|
| 302 |
+
self._last_files[path] = set(os.listdir(path))
|
| 303 |
+
|
| 304 |
+
# Always emit validation and state changes
|
| 305 |
self.db_validation_changed.emit(is_valid, message)
|
| 306 |
+
self.db_state_changed.emit(is_valid, message, changes)
|
| 307 |
|
| 308 |
+
# If changes detected, emit files changed signal
|
| 309 |
+
if changes:
|
| 310 |
self.logger.debug(f"Detected file changes: {changes}")
|
| 311 |
self.db_files_changed.emit(changes)
|
|
|
|
|
|
|
|
|
|
| 312 |
|
|
|
|
|
|
|
| 313 |
except Exception as e:
|
| 314 |
self.logger.error(f"Error handling directory change: {str(e)}")
|
| 315 |
|
|
|
|
| 331 |
if f.endswith('.gbff')]
|
| 332 |
|
| 333 |
def check_db_state(self):
|
| 334 |
+
"""
|
| 335 |
+
Check database state without clearing invalid paths
|
| 336 |
+
"""
|
| 337 |
+
try:
|
| 338 |
+
current_path = self.get_db_path()
|
| 339 |
+
is_valid, message = self.validate_db_path(current_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
+
# Get list of changes if path is valid
|
| 342 |
+
changes = {} # Initialize as dict instead of list
|
| 343 |
+
if is_valid:
|
| 344 |
+
changes = self._detect_file_changes()
|
| 345 |
+
|
| 346 |
+
# Emit signals but don't modify the path
|
| 347 |
+
self.db_validation_changed.emit(is_valid, message)
|
| 348 |
+
self.db_state_changed.emit(is_valid, message, changes)
|
| 349 |
+
|
| 350 |
+
except Exception as e:
|
| 351 |
+
self.logger.error(f"Error checking database state: {str(e)}")
|
| 352 |
|
| 353 |
def get_organisms_and_endos(self):
|
| 354 |
"""Get mapping of organisms to their endonucleases and files"""
|
|
|
|
| 613 |
except Exception as e:
|
| 614 |
self.logger.error(f"Error calculating statistics: {str(e)}")
|
| 615 |
raise
|
| 616 |
+
|
| 617 |
+
def update_db_state(self):
|
| 618 |
+
"""Check and update the database state"""
|
| 619 |
+
self.logger.debug("Checking database state")
|
| 620 |
+
if not self.db_path and not self.pending_db_path:
|
| 621 |
+
self.load_database_path()
|
| 622 |
+
|
| 623 |
+
# Use active path for validation
|
| 624 |
+
path_to_check = self.get_active_db_path()
|
| 625 |
+
self.logger.debug(f"Checking state for path: {path_to_check} (pending: {self.pending_db_path}, current: {self.db_path})")
|
| 626 |
+
|
| 627 |
+
is_valid, message = self.validate_db_path(path_to_check)
|
| 628 |
+
self.logger.debug(f"Database validation result - Path: {path_to_check}, Valid: {is_valid}, Message: {message}")
|
| 629 |
+
|
| 630 |
+
# Detect any changes since last check
|
| 631 |
+
changes = self._detect_file_changes()
|
| 632 |
+
|
| 633 |
+
# Check if we should finalize a directory change
|
| 634 |
+
if self.pending_db_path and self.is_changing_directory:
|
| 635 |
+
self.logger.debug("Checking conditions for directory change finalization")
|
| 636 |
+
self.logger.debug(f"Is valid: {is_valid}, Has changes: {bool(changes)}")
|
| 637 |
+
|
| 638 |
+
if is_valid and not changes: # No changes means we're not in the middle of file operations
|
| 639 |
+
self.logger.debug("Attempting to finalize directory change")
|
| 640 |
+
success, finalize_message = self.finalize_directory_change()
|
| 641 |
+
if success:
|
| 642 |
+
self.logger.debug("Directory change finalized successfully")
|
| 643 |
+
# Emit signals
|
| 644 |
+
self.db_validation_changed.emit(True, finalize_message)
|
| 645 |
+
self.db_state_changed.emit(True, finalize_message, changes)
|
| 646 |
+
return
|
| 647 |
+
else:
|
| 648 |
+
self.logger.debug(f"Directory change finalization failed: {finalize_message}")
|
| 649 |
+
|
| 650 |
+
# Emit regular signals
|
| 651 |
+
self.db_validation_changed.emit(is_valid, message)
|
| 652 |
+
if changes:
|
| 653 |
+
self.db_files_changed.emit(changes)
|
| 654 |
+
|
| 655 |
+
self.logger.info(f"Database state checked - Valid: {is_valid}, Changes: {changes}")
|
| 656 |
+
|
| 657 |
+
def cancel_directory_change(self):
|
| 658 |
+
"""Cancel the directory change process"""
|
| 659 |
+
self.logger.debug(f"Cancelling directory change process. Previous state - Pending: {self.pending_db_path}, Changing: {self.is_changing_directory}")
|
| 660 |
+
self.pending_db_path = None
|
| 661 |
+
self.is_changing_directory = False
|
| 662 |
+
self.logger.debug("Directory change process cancelled")
|
| 663 |
+
|
| 664 |
+
def finalize_directory_change(self):
|
| 665 |
+
"""Finalize the database directory change after successful validation"""
|
| 666 |
+
try:
|
| 667 |
+
if self.pending_db_path and self.is_changing_directory:
|
| 668 |
+
self.logger.debug(f"Finalizing directory change from {self.db_path} to {self.pending_db_path}")
|
| 669 |
+
|
| 670 |
+
# Validate the pending path one final time
|
| 671 |
+
is_valid, message = self.validate_db_path(self.pending_db_path)
|
| 672 |
+
if not is_valid:
|
| 673 |
+
self.logger.warning(f"Cannot finalize directory change: {message}")
|
| 674 |
+
return False, message
|
| 675 |
+
|
| 676 |
+
# Update the current path
|
| 677 |
+
old_path = self.db_path
|
| 678 |
+
self.db_path = self.pending_db_path
|
| 679 |
+
|
| 680 |
+
# Update .env file - try multiple approaches to ensure it works
|
| 681 |
+
try:
|
| 682 |
+
# First attempt: Direct write
|
| 683 |
+
self.config_manager.write_to_env('CSPR_DB', self.db_path)
|
| 684 |
+
|
| 685 |
+
# Second attempt: Use set_env_value
|
| 686 |
+
self.config_manager.set_env_value('CSPR_DB', self.db_path)
|
| 687 |
+
|
| 688 |
+
# Force reload environment variables
|
| 689 |
+
self.config_manager.load_env()
|
| 690 |
+
|
| 691 |
+
# Verify the update
|
| 692 |
+
new_env_value = self.config_manager.get_env_value('CSPR_DB')
|
| 693 |
+
if new_env_value != self.db_path:
|
| 694 |
+
raise Exception(f"Environment variable update failed. Expected: {self.db_path}, Got: {new_env_value}")
|
| 695 |
+
|
| 696 |
+
except Exception as e:
|
| 697 |
+
self.logger.error(f"Error updating environment variable: {str(e)}")
|
| 698 |
+
return False, f"Failed to update environment variable: {str(e)}"
|
| 699 |
+
|
| 700 |
+
# Clear pending state
|
| 701 |
+
self.pending_db_path = None
|
| 702 |
+
self.is_changing_directory = False
|
| 703 |
+
|
| 704 |
+
# Update database state
|
| 705 |
+
self.update_db_state()
|
| 706 |
+
|
| 707 |
+
success_message = f"Successfully changed database directory to:\n{self.db_path}"
|
| 708 |
+
self.logger.info(f"Successfully changed database directory from {old_path} to {self.db_path}")
|
| 709 |
+
self.logger.debug("Directory change finalized successfully")
|
| 710 |
+
|
| 711 |
+
return True, success_message
|
| 712 |
+
|
| 713 |
+
return False, "No pending directory change to finalize"
|
| 714 |
+
|
| 715 |
+
except Exception as e:
|
| 716 |
+
self.logger.error(f"Error finalizing directory change: {str(e)}")
|
| 717 |
+
return False, f"Error finalizing directory change: {str(e)}"
|
|
@@ -1,21 +1,19 @@
|
|
| 1 |
-
from models.HomeWindowModel import HomeWindowModel
|
| 2 |
from models.CSPRparser import CSPRparser
|
|
|
|
| 3 |
from models.AnnotationParser import AnnotationParser
|
| 4 |
import os
|
| 5 |
from functools import lru_cache
|
| 6 |
import traceback
|
| 7 |
from Bio import SeqIO
|
| 8 |
|
| 9 |
-
class FindTargetsModel(
|
| 10 |
def __init__(self, global_settings):
|
| 11 |
super().__init__(global_settings)
|
| 12 |
self.results = {}
|
| 13 |
-
self._parser_cache = {}
|
| 14 |
-
self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
|
| 15 |
|
| 16 |
-
def
|
| 17 |
-
"""Clear
|
| 18 |
-
self.global_settings.logger.debug(f"FindTargetsModel clearing caches for new annotation file: {new_annotation_file}")
|
| 19 |
self._parser_cache.clear()
|
| 20 |
|
| 21 |
@lru_cache(maxsize=32)
|
|
@@ -26,11 +24,9 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 26 |
return self._parser_cache[file_path]
|
| 27 |
|
| 28 |
def find_targets(self, input_data):
|
| 29 |
-
self.global_settings.logger.debug(f"Received input data: {input_data}")
|
| 30 |
-
|
| 31 |
organism = input_data['organism']
|
| 32 |
endo = input_data['endonuclease']
|
| 33 |
-
org_files = self.
|
| 34 |
|
| 35 |
self._validate_input(organism, endo, org_files)
|
| 36 |
|
|
@@ -46,7 +42,7 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 46 |
search_func = search_types.get(input_data['search_type'])
|
| 47 |
if not search_func:
|
| 48 |
error_msg = f"Invalid search type: {input_data['search_type']}"
|
| 49 |
-
self.
|
| 50 |
raise ValueError(error_msg)
|
| 51 |
|
| 52 |
self.results = search_func(parser, input_data)
|
|
@@ -56,12 +52,12 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 56 |
def _validate_input(self, organism, endo, org_files):
|
| 57 |
if organism not in org_files:
|
| 58 |
error_msg = f"Organism '{organism}' not found in the database. Available organisms: {list(org_files.keys())}"
|
| 59 |
-
self.
|
| 60 |
raise ValueError(error_msg)
|
| 61 |
|
| 62 |
if endo not in org_files[organism]:
|
| 63 |
error_msg = f"Endonuclease '{endo}' not found for organism '{organism}'. Available endonucleases: {list(org_files[organism].keys())}"
|
| 64 |
-
self.
|
| 65 |
raise ValueError(error_msg)
|
| 66 |
|
| 67 |
def find_targets_by_feature(self, parser, input_data):
|
|
@@ -84,18 +80,17 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 84 |
self.global_settings.logger.error(f"Annotation file not found: {annotation_file_path}")
|
| 85 |
raise FileNotFoundError(f"Annotation file not found: {annotation_file_path}")
|
| 86 |
|
| 87 |
-
self.global_settings.logger.debug(f"Using annotation file: {annotation_file_path}")
|
| 88 |
-
|
| 89 |
# Split search queries by newlines and remove empty lines
|
| 90 |
search_queries = [q.strip() for q in input_data['search_query'].split('\n') if q.strip()]
|
| 91 |
|
| 92 |
-
|
| 93 |
-
annotation_parser.
|
|
|
|
| 94 |
|
| 95 |
# Process each query and combine results
|
| 96 |
all_results = []
|
| 97 |
for search_query in search_queries:
|
| 98 |
-
results_list = annotation_parser.genbank_search([search_query])
|
| 99 |
|
| 100 |
for record_id, feature_info in results_list:
|
| 101 |
location = feature_info['feature_location']
|
|
@@ -355,3 +350,11 @@ class FindTargetsModel(HomeWindowModel):
|
|
| 355 |
'endonuclease': target[5]
|
| 356 |
})
|
| 357 |
return formatted_results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from models.CSPRparser import CSPRparser
|
| 2 |
+
from models.BaseModel import BaseModel
|
| 3 |
from models.AnnotationParser import AnnotationParser
|
| 4 |
import os
|
| 5 |
from functools import lru_cache
|
| 6 |
import traceback
|
| 7 |
from Bio import SeqIO
|
| 8 |
|
| 9 |
+
class FindTargetsModel(BaseModel):
|
| 10 |
def __init__(self, global_settings):
|
| 11 |
super().__init__(global_settings)
|
| 12 |
self.results = {}
|
| 13 |
+
self._parser_cache = {}
|
|
|
|
| 14 |
|
| 15 |
+
def _clear_caches(self):
|
| 16 |
+
"""Clear all model-specific caches"""
|
|
|
|
| 17 |
self._parser_cache.clear()
|
| 18 |
|
| 19 |
@lru_cache(maxsize=32)
|
|
|
|
| 24 |
return self._parser_cache[file_path]
|
| 25 |
|
| 26 |
def find_targets(self, input_data):
|
|
|
|
|
|
|
| 27 |
organism = input_data['organism']
|
| 28 |
endo = input_data['endonuclease']
|
| 29 |
+
org_files = self.global_settings.get_organism_files()
|
| 30 |
|
| 31 |
self._validate_input(organism, endo, org_files)
|
| 32 |
|
|
|
|
| 42 |
search_func = search_types.get(input_data['search_type'])
|
| 43 |
if not search_func:
|
| 44 |
error_msg = f"Invalid search type: {input_data['search_type']}"
|
| 45 |
+
self.logger.error(error_msg)
|
| 46 |
raise ValueError(error_msg)
|
| 47 |
|
| 48 |
self.results = search_func(parser, input_data)
|
|
|
|
| 52 |
def _validate_input(self, organism, endo, org_files):
|
| 53 |
if organism not in org_files:
|
| 54 |
error_msg = f"Organism '{organism}' not found in the database. Available organisms: {list(org_files.keys())}"
|
| 55 |
+
self.logger.error(error_msg)
|
| 56 |
raise ValueError(error_msg)
|
| 57 |
|
| 58 |
if endo not in org_files[organism]:
|
| 59 |
error_msg = f"Endonuclease '{endo}' not found for organism '{organism}'. Available endonucleases: {list(org_files[organism].keys())}"
|
| 60 |
+
self.logger.error(error_msg)
|
| 61 |
raise ValueError(error_msg)
|
| 62 |
|
| 63 |
def find_targets_by_feature(self, parser, input_data):
|
|
|
|
| 80 |
self.global_settings.logger.error(f"Annotation file not found: {annotation_file_path}")
|
| 81 |
raise FileNotFoundError(f"Annotation file not found: {annotation_file_path}")
|
| 82 |
|
|
|
|
|
|
|
| 83 |
# Split search queries by newlines and remove empty lines
|
| 84 |
search_queries = [q.strip() for q in input_data['search_query'].split('\n') if q.strip()]
|
| 85 |
|
| 86 |
+
# Use existing annotation parser from BaseModel
|
| 87 |
+
if self.annotation_parser.annotation_file_name != annotation_file_path:
|
| 88 |
+
self.annotation_parser.set_annotation_file(annotation_file_path)
|
| 89 |
|
| 90 |
# Process each query and combine results
|
| 91 |
all_results = []
|
| 92 |
for search_query in search_queries:
|
| 93 |
+
results_list = self.annotation_parser.genbank_search([search_query])
|
| 94 |
|
| 95 |
for record_id, feature_info in results_list:
|
| 96 |
location = feature_info['feature_location']
|
|
|
|
| 350 |
'endonuclease': target[5]
|
| 351 |
})
|
| 352 |
return formatted_results
|
| 353 |
+
|
| 354 |
+
def get_cspr_file_path(self, input_data):
|
| 355 |
+
"""Get the path to the CSPR file for the given input data"""
|
| 356 |
+
org_files = self.global_settings.get_organism_files()
|
| 357 |
+
return os.path.join(
|
| 358 |
+
self.global_settings.get_db_path(),
|
| 359 |
+
org_files[input_data['organism']][input_data['endonuclease']][0]
|
| 360 |
+
)
|
|
@@ -1,21 +1,30 @@
|
|
| 1 |
from models.CSPRparser import CSPRparser
|
| 2 |
-
from models.
|
| 3 |
import os
|
| 4 |
import re
|
| 5 |
import traceback
|
|
|
|
| 6 |
|
| 7 |
-
class GenerateLibraryModel(
|
|
|
|
|
|
|
| 8 |
def __init__(self, global_settings):
|
| 9 |
-
super().__init__(
|
|
|
|
| 10 |
self.logger = global_settings.logger
|
| 11 |
self.parser = None
|
| 12 |
self.targets_data = {}
|
| 13 |
self._deleted_targets = {}
|
|
|
|
| 14 |
|
| 15 |
def initialize_parser(self, cspr_file):
|
| 16 |
"""Initialize CSPR parser"""
|
| 17 |
self.parser = CSPRparser(cspr_file, self.global_settings.get_casper_info_path())
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
def generate_library(self, selected_targets, settings):
|
| 20 |
"""Generate library with given settings"""
|
| 21 |
try:
|
|
@@ -29,26 +38,114 @@ class GenerateLibraryModel(HomeWindowModel):
|
|
| 29 |
settings['target_range_start'],
|
| 30 |
settings['target_range_end']
|
| 31 |
)
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
output_data = self._generate_output(
|
| 35 |
processed_targets,
|
| 36 |
settings['guides_per_gene'],
|
| 37 |
settings['space_between_guides']
|
| 38 |
)
|
| 39 |
-
|
| 40 |
-
self.logger.debug(f"Output data: {output_data}")
|
| 41 |
|
| 42 |
# Write output to file
|
| 43 |
self._write_output(output_data, settings)
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
except Exception as e:
|
| 48 |
-
self.logger.error(f"Error
|
| 49 |
-
self.logger.error(traceback.format_exc())
|
| 50 |
raise
|
| 51 |
-
|
| 52 |
def _process_targets(self, targets, min_score, five_prime_seq, start_range, end_range):
|
| 53 |
"""Process and filter targets based on criteria"""
|
| 54 |
processed = {}
|
|
@@ -73,17 +170,11 @@ class GenerateLibraryModel(HomeWindowModel):
|
|
| 73 |
else:
|
| 74 |
self._deleted_targets[gene_name].append(target_data)
|
| 75 |
|
| 76 |
-
#
|
| 77 |
for gene in processed:
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# Then sort by position (ascending)
|
| 82 |
-
processed[gene].sort(key=lambda x: abs(int(x['position'])))
|
| 83 |
-
|
| 84 |
-
# Reverse list if gene is on negative strand
|
| 85 |
-
if processed[gene] and processed[gene][0].get('strand', '+') == '-':
|
| 86 |
-
processed[gene].reverse()
|
| 87 |
|
| 88 |
return processed
|
| 89 |
|
|
@@ -93,14 +184,18 @@ class GenerateLibraryModel(HomeWindowModel):
|
|
| 93 |
# Score filter - convert score to float and compare
|
| 94 |
target_score = float(target.get('score', 0))
|
| 95 |
if target_score < min_score:
|
| 96 |
-
self.logger.debug(f"Target failed score filter: {target_score} < {min_score}")
|
| 97 |
return False
|
| 98 |
|
| 99 |
-
|
|
|
|
|
|
|
| 100 |
if re.search("T{5,10}", target['sequence']):
|
| 101 |
self.logger.debug(f"Target failed poly-T filter: {target['sequence']}")
|
| 102 |
return False
|
| 103 |
|
|
|
|
|
|
|
| 104 |
# 5' sequence filter
|
| 105 |
if five_prime_seq and not target['sequence'].startswith(five_prime_seq.upper()):
|
| 106 |
self.logger.debug(f"Target failed 5' sequence filter")
|
|
@@ -146,47 +241,77 @@ class GenerateLibraryModel(HomeWindowModel):
|
|
| 146 |
return 0
|
| 147 |
|
| 148 |
def _generate_output(self, processed_targets, guides_per_gene, space_between):
|
|
|
|
| 149 |
output = {}
|
| 150 |
|
| 151 |
for gene_id, targets in processed_targets.items():
|
| 152 |
output[gene_id] = []
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
prev_target = None
|
| 156 |
|
| 157 |
while i < guides_per_gene:
|
| 158 |
if len(targets) == 0 or vec_index >= len(targets):
|
| 159 |
break
|
| 160 |
-
|
| 161 |
current = targets[vec_index]
|
| 162 |
|
| 163 |
-
#
|
| 164 |
-
if prev_target is None
|
| 165 |
-
|
| 166 |
-
if (prev_target and float(current['score']) > float(prev_target['score'])):
|
| 167 |
-
output[gene_id].pop()
|
| 168 |
-
output[gene_id].append(current)
|
| 169 |
-
else:
|
| 170 |
-
output[gene_id].append(current)
|
| 171 |
prev_target = current
|
| 172 |
i += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
vec_index += 1
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
key=lambda x: (float(x['score']), abs(int(x['position'])))
|
| 183 |
-
)
|
| 184 |
|
| 185 |
-
|
| 186 |
-
if len(output[gene_id]) >= guides_per_gene:
|
| 187 |
-
break
|
| 188 |
-
deleted_target['modified'] = True
|
| 189 |
-
output[gene_id].append(deleted_target)
|
| 190 |
|
| 191 |
return output
|
| 192 |
|
|
|
|
| 1 |
from models.CSPRparser import CSPRparser
|
| 2 |
+
from models.OffTargetModel import OffTargetModel
|
| 3 |
import os
|
| 4 |
import re
|
| 5 |
import traceback
|
| 6 |
+
from PyQt6.QtCore import QObject, pyqtSignal
|
| 7 |
|
| 8 |
+
class GenerateLibraryModel(QObject):
|
| 9 |
+
progress_updated = pyqtSignal(int) # Signal to emit progress updates
|
| 10 |
+
|
| 11 |
def __init__(self, global_settings):
|
| 12 |
+
super().__init__()
|
| 13 |
+
self.global_settings = global_settings
|
| 14 |
self.logger = global_settings.logger
|
| 15 |
self.parser = None
|
| 16 |
self.targets_data = {}
|
| 17 |
self._deleted_targets = {}
|
| 18 |
+
self.off_target_model = OffTargetModel(global_settings)
|
| 19 |
|
| 20 |
def initialize_parser(self, cspr_file):
|
| 21 |
"""Initialize CSPR parser"""
|
| 22 |
self.parser = CSPRparser(cspr_file, self.global_settings.get_casper_info_path())
|
| 23 |
|
| 24 |
+
def get_organism_to_files(self):
|
| 25 |
+
"""Get mapping of organisms to their files from global settings"""
|
| 26 |
+
return self.global_settings.get_organism_files()
|
| 27 |
+
|
| 28 |
def generate_library(self, selected_targets, settings):
|
| 29 |
"""Generate library with given settings"""
|
| 30 |
try:
|
|
|
|
| 38 |
settings['target_range_start'],
|
| 39 |
settings['target_range_end']
|
| 40 |
)
|
| 41 |
+
|
| 42 |
+
if settings.get('find_off_targets'):
|
| 43 |
+
# Write targets to temp file for off-target analysis
|
| 44 |
+
self._write_targets_to_temp(processed_targets)
|
| 45 |
+
|
| 46 |
+
# Get organism and endonuclease from home window
|
| 47 |
+
if hasattr(self.global_settings, '_current_home_window'):
|
| 48 |
+
organism = self.global_settings._current_home_window.view.combo_box_organism.currentText()
|
| 49 |
+
endonuclease = self.global_settings._current_home_window.view.combo_box_endonuclease.currentText()
|
| 50 |
+
else:
|
| 51 |
+
raise ValueError("Could not access home window to get organism and endonuclease")
|
| 52 |
+
|
| 53 |
+
if not organism or not endonuclease:
|
| 54 |
+
raise ValueError("Could not determine organism or endonuclease from home window")
|
| 55 |
+
|
| 56 |
+
self.logger.debug(f"Using organism: {organism} and endonuclease: {endonuclease} for off-target analysis")
|
| 57 |
+
|
| 58 |
+
# Setup off-target parameters
|
| 59 |
+
off_target_params = {
|
| 60 |
+
'organism': organism,
|
| 61 |
+
'endonuclease': endonuclease,
|
| 62 |
+
'max_mismatches': 4, # Default value from old implementation
|
| 63 |
+
'tolerance': 0.05, # Default value from old implementation
|
| 64 |
+
'average_output': True,
|
| 65 |
+
'save_output': False,
|
| 66 |
+
'output_filename': '',
|
| 67 |
+
'targets': selected_targets,
|
| 68 |
+
'annotation_file': self.global_settings.get_current_annotation_file()
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# Connect to off-target model signals
|
| 72 |
+
self.off_target_model.progress_updated.connect(self._handle_off_target_progress)
|
| 73 |
+
self.off_target_model.results_ready.connect(lambda results: self._handle_off_target_results(results, processed_targets, settings))
|
| 74 |
+
|
| 75 |
+
# Start off-target analysis
|
| 76 |
+
self.off_target_model.start_analysis(off_target_params)
|
| 77 |
+
return True
|
| 78 |
+
else:
|
| 79 |
+
# Generate output for each target
|
| 80 |
+
output_data = self._generate_output(
|
| 81 |
+
processed_targets,
|
| 82 |
+
settings['guides_per_gene'],
|
| 83 |
+
settings['space_between_guides']
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
self.logger.debug(f"Output data: {output_data}")
|
| 87 |
+
|
| 88 |
+
# Write output to file
|
| 89 |
+
self._write_output(output_data, settings)
|
| 90 |
+
return True
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
self.logger.error(f"Error generating library: {str(e)}")
|
| 94 |
+
self.logger.error(traceback.format_exc())
|
| 95 |
+
raise
|
| 96 |
+
|
| 97 |
+
def _write_targets_to_temp(self, processed_targets):
|
| 98 |
+
"""Write targets to temp file for off-target analysis"""
|
| 99 |
+
try:
|
| 100 |
+
temp_path = os.path.join(self.global_settings.get_db_path(), 'temp.txt')
|
| 101 |
+
|
| 102 |
+
with open(temp_path, 'w') as f:
|
| 103 |
+
for gene in processed_targets:
|
| 104 |
+
for target in processed_targets[gene]:
|
| 105 |
+
# Format: position;sequence;pam;score;strand
|
| 106 |
+
entry = f"{target['position']};{target['sequence']};{target['pam']};{target['score']};{target['strand']}\n"
|
| 107 |
+
f.write(entry)
|
| 108 |
+
|
| 109 |
+
self.logger.debug(f"Wrote targets to temp file: {temp_path}")
|
| 110 |
+
|
| 111 |
+
except Exception as e:
|
| 112 |
+
self.logger.error(f"Error writing targets to temp file: {str(e)}")
|
| 113 |
+
raise
|
| 114 |
+
|
| 115 |
+
def _handle_off_target_progress(self, value, status):
|
| 116 |
+
"""Handle progress updates from off-target analysis"""
|
| 117 |
+
self.progress_updated.emit(value)
|
| 118 |
+
|
| 119 |
+
def _handle_off_target_results(self, results, processed_targets, settings):
|
| 120 |
+
"""Handle results from off-target analysis"""
|
| 121 |
+
try:
|
| 122 |
+
scores_dict, _ = results
|
| 123 |
+
|
| 124 |
+
# Update targets with off-target scores
|
| 125 |
+
for gene in processed_targets:
|
| 126 |
+
for target in processed_targets[gene]:
|
| 127 |
+
if target['sequence'] in scores_dict:
|
| 128 |
+
target['off_target_score'] = scores_dict[target['sequence']]
|
| 129 |
+
|
| 130 |
+
# Generate output with updated targets
|
| 131 |
output_data = self._generate_output(
|
| 132 |
processed_targets,
|
| 133 |
settings['guides_per_gene'],
|
| 134 |
settings['space_between_guides']
|
| 135 |
)
|
|
|
|
|
|
|
| 136 |
|
| 137 |
# Write output to file
|
| 138 |
self._write_output(output_data, settings)
|
| 139 |
|
| 140 |
+
# Clean up temp file
|
| 141 |
+
temp_path = os.path.join(self.global_settings.get_db_path(), 'temp.txt')
|
| 142 |
+
if os.path.exists(temp_path):
|
| 143 |
+
os.remove(temp_path)
|
| 144 |
+
|
| 145 |
except Exception as e:
|
| 146 |
+
self.logger.error(f"Error handling off-target results: {str(e)}")
|
|
|
|
| 147 |
raise
|
| 148 |
+
|
| 149 |
def _process_targets(self, targets, min_score, five_prime_seq, start_range, end_range):
|
| 150 |
"""Process and filter targets based on criteria"""
|
| 151 |
processed = {}
|
|
|
|
| 170 |
else:
|
| 171 |
self._deleted_targets[gene_name].append(target_data)
|
| 172 |
|
| 173 |
+
# Log first 5 targets for each gene for debugging
|
| 174 |
for gene in processed:
|
| 175 |
+
if processed[gene]:
|
| 176 |
+
self.logger.debug(f"First 5 targets for gene {gene}: {[t['score'] for t in processed[gene][:5]]}")
|
| 177 |
+
self.logger.debug(f"First 5 target positions for gene {gene}: {[t['position'] for t in processed[gene][:5]]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
return processed
|
| 180 |
|
|
|
|
| 184 |
# Score filter - convert score to float and compare
|
| 185 |
target_score = float(target.get('score', 0))
|
| 186 |
if target_score < min_score:
|
| 187 |
+
self.logger.debug(f"Target failed score filter: {target_score} < {min_score}, target: {target}")
|
| 188 |
return False
|
| 189 |
|
| 190 |
+
self.logger.debug(f"Target passed score filter: {target_score} >= {min_score}, target: {target}")
|
| 191 |
+
|
| 192 |
+
# Poly-T filter (5-10 consecutive T's)
|
| 193 |
if re.search("T{5,10}", target['sequence']):
|
| 194 |
self.logger.debug(f"Target failed poly-T filter: {target['sequence']}")
|
| 195 |
return False
|
| 196 |
|
| 197 |
+
self.logger.debug(f"Target passed poly-T filter: {target['sequence']}")
|
| 198 |
+
|
| 199 |
# 5' sequence filter
|
| 200 |
if five_prime_seq and not target['sequence'].startswith(five_prime_seq.upper()):
|
| 201 |
self.logger.debug(f"Target failed 5' sequence filter")
|
|
|
|
| 241 |
return 0
|
| 242 |
|
| 243 |
def _generate_output(self, processed_targets, guides_per_gene, space_between):
|
| 244 |
+
"""Generate output with proper spacing between guides"""
|
| 245 |
output = {}
|
| 246 |
|
| 247 |
for gene_id, targets in processed_targets.items():
|
| 248 |
output[gene_id] = []
|
| 249 |
+
|
| 250 |
+
# First sort by score (descending)
|
| 251 |
+
targets.sort(key=lambda x: float(x['score']), reverse=True)
|
| 252 |
+
self.logger.debug(f"First 5 targets positions for gene {gene_id} by score: {[(t['position'], t['score']) for t in targets[:5]]}")
|
| 253 |
+
|
| 254 |
+
# Then sort by position
|
| 255 |
+
targets.sort(key=lambda x: abs(int(x['position'])))
|
| 256 |
+
self.logger.debug(f"First 5 targets positions for gene {gene_id}: {[t['position'] for t in targets[:5]]}")
|
| 257 |
+
|
| 258 |
+
i = 0 # Counter for selected guides
|
| 259 |
+
vec_index = 0 # Index for current target being considered
|
| 260 |
prev_target = None
|
| 261 |
|
| 262 |
while i < guides_per_gene:
|
| 263 |
if len(targets) == 0 or vec_index >= len(targets):
|
| 264 |
break
|
| 265 |
+
|
| 266 |
current = targets[vec_index]
|
| 267 |
|
| 268 |
+
# For first target, just add it
|
| 269 |
+
if prev_target is None:
|
| 270 |
+
output[gene_id].append(current)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
prev_target = current
|
| 272 |
i += 1
|
| 273 |
+
else:
|
| 274 |
+
# Check spacing from previous target
|
| 275 |
+
distance = abs(int(current['position']) - int(prev_target['position']))
|
| 276 |
+
|
| 277 |
+
if distance >= space_between:
|
| 278 |
+
# Look ahead for better scoring targets within this space
|
| 279 |
+
best_target = current
|
| 280 |
+
look_ahead_index = vec_index + 1
|
| 281 |
+
|
| 282 |
+
while look_ahead_index < len(targets):
|
| 283 |
+
next_target = targets[look_ahead_index]
|
| 284 |
+
next_distance = abs(int(next_target['position']) - int(prev_target['position']))
|
| 285 |
+
|
| 286 |
+
# If we've gone too far, break
|
| 287 |
+
if next_distance >= space_between:
|
| 288 |
+
break
|
| 289 |
+
|
| 290 |
+
# If this target has better score
|
| 291 |
+
if float(next_target['score']) > float(best_target['score']):
|
| 292 |
+
best_target = next_target
|
| 293 |
+
|
| 294 |
+
look_ahead_index += 1
|
| 295 |
+
|
| 296 |
+
output[gene_id].append(best_target)
|
| 297 |
+
prev_target = best_target
|
| 298 |
+
i += 1
|
| 299 |
+
|
| 300 |
+
# Move vec_index past the selected target's position
|
| 301 |
+
while vec_index < len(targets) and abs(int(targets[vec_index]['position'])) <= abs(int(best_target['position'])):
|
| 302 |
+
vec_index += 1
|
| 303 |
+
continue
|
| 304 |
|
| 305 |
vec_index += 1
|
| 306 |
+
|
| 307 |
+
# Sort final output by position
|
| 308 |
+
output[gene_id].sort(key=lambda x: abs(int(x['position'])))
|
| 309 |
+
|
| 310 |
+
# If gene is on negative strand, reverse the order
|
| 311 |
+
if output[gene_id] and output[gene_id][0].get('strand', '+') == '-':
|
| 312 |
+
output[gene_id].reverse()
|
|
|
|
|
|
|
| 313 |
|
| 314 |
+
self.logger.debug(f"Selected targets positions for gene {gene_id}: {[t['position'] for t in output[gene_id]]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
return output
|
| 317 |
|
|
@@ -68,21 +68,46 @@ class GlobalSettings(QObject):
|
|
| 68 |
self._preloading_modules = {}
|
| 69 |
self.main_window = None
|
| 70 |
|
| 71 |
-
#
|
| 72 |
-
self._preload_essential_controllers()
|
| 73 |
-
|
| 74 |
-
# Start background loading of commonly used modules
|
| 75 |
-
self._background_load_common_modules()
|
| 76 |
-
|
| 77 |
self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
|
| 78 |
self.config_manager.load_env()
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
|
| 81 |
|
| 82 |
-
#
|
| 83 |
-
self._initialize_directories()
|
| 84 |
self._init_db_manager()
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
# Defer theme initialization
|
| 87 |
self._init_theme_settings()
|
| 88 |
|
|
@@ -170,6 +195,8 @@ class GlobalSettings(QObject):
|
|
| 170 |
|
| 171 |
def validate_db_path(self, path):
|
| 172 |
"""Validate the given database path"""
|
|
|
|
|
|
|
| 173 |
return self.db_manager.validate_db_path(path)
|
| 174 |
|
| 175 |
def save_db_path(self, path):
|
|
@@ -295,8 +322,6 @@ class GlobalSettings(QObject):
|
|
| 295 |
def _get_window_class(self, window_name):
|
| 296 |
"""Get the controller class with optimized loading"""
|
| 297 |
try:
|
| 298 |
-
start_time = time.time()
|
| 299 |
-
|
| 300 |
# Check if module is already cached
|
| 301 |
module_path = f"controllers.{window_name}Controller"
|
| 302 |
if module_path in self._module_cache:
|
|
@@ -329,7 +354,6 @@ class GlobalSettings(QObject):
|
|
| 329 |
if not hasattr(controller_module, class_name):
|
| 330 |
raise AttributeError(f"Controller module does not contain class {class_name}")
|
| 331 |
|
| 332 |
-
self.logger.debug(f"Window class retrieval took: {time.time() - start_time:.2f} seconds")
|
| 333 |
return getattr(controller_module, class_name)
|
| 334 |
|
| 335 |
except Exception as e:
|
|
@@ -350,11 +374,15 @@ class GlobalSettings(QObject):
|
|
| 350 |
self.logger.error(f"Error creating window {window_name}: {str(e)}")
|
| 351 |
raise
|
| 352 |
|
| 353 |
-
def get_startup_window(self):
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
def get_home_window(self):
|
| 360 |
"""Get or create home window with proper initialization"""
|
|
@@ -391,7 +419,13 @@ class GlobalSettings(QObject):
|
|
| 391 |
def _background_load_common_modules(self):
|
| 392 |
"""Start background loading of commonly used modules"""
|
| 393 |
try:
|
| 394 |
-
common_modules = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
for module_name in common_modules:
|
| 396 |
if (module_name not in self._module_cache and
|
| 397 |
module_name not in self._preloading_modules):
|
|
@@ -411,7 +445,6 @@ class GlobalSettings(QObject):
|
|
| 411 |
preloader = self._preloading_modules[module_name]
|
| 412 |
if not preloader.isRunning(): # Only remove if thread is finished
|
| 413 |
del self._preloading_modules[module_name]
|
| 414 |
-
self.logger.debug(f"Module {module_name} preloaded successfully")
|
| 415 |
except Exception as e:
|
| 416 |
self.logger.error(f"Error handling preloaded module: {str(e)}")
|
| 417 |
|
|
@@ -501,29 +534,15 @@ class GlobalSettings(QObject):
|
|
| 501 |
def set_current_annotation_file(self, annotation_file):
|
| 502 |
"""Set the current annotation file and notify listeners"""
|
| 503 |
try:
|
| 504 |
-
if not hasattr(self, '_current_annotation_file'):
|
| 505 |
-
self._current_annotation_file = None
|
| 506 |
-
|
| 507 |
if self._current_annotation_file != annotation_file:
|
| 508 |
self._current_annotation_file = annotation_file
|
| 509 |
-
self.logger.debug(f"Current annotation file changed to: {annotation_file}")
|
| 510 |
self.annotation_file_changed.emit(annotation_file)
|
| 511 |
except Exception as e:
|
| 512 |
self.logger.error(f"Error setting current annotation file: {str(e)}")
|
| 513 |
|
| 514 |
def get_current_annotation_file(self):
|
| 515 |
"""Get the currently selected annotation file"""
|
| 516 |
-
|
| 517 |
-
if not self._current_annotation_file and hasattr(self, '_current_home_window'):
|
| 518 |
-
# Try to get from home window if not set
|
| 519 |
-
home_controller = self._current_home_window
|
| 520 |
-
if hasattr(home_controller, 'view'):
|
| 521 |
-
self._current_annotation_file = home_controller.view.get_annotation_file()
|
| 522 |
-
self.logger.debug(f"Got annotation file from home window: {self._current_annotation_file}")
|
| 523 |
-
return self._current_annotation_file
|
| 524 |
-
except Exception as e:
|
| 525 |
-
self.logger.error(f"Error getting current annotation file: {str(e)}")
|
| 526 |
-
return None
|
| 527 |
|
| 528 |
def get_scoring_options_window(self, view_targets_controller):
|
| 529 |
"""Create and return ScoringOptionsController instance"""
|
|
@@ -575,5 +594,125 @@ class GlobalSettings(QObject):
|
|
| 575 |
self.logger.error(f"Error adjusting path: {str(e)}")
|
| 576 |
return path # Return original path if adjustment fails
|
| 577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
# Global instance
|
| 579 |
global_settings = None
|
|
|
|
| 68 |
self._preloading_modules = {}
|
| 69 |
self.main_window = None
|
| 70 |
|
| 71 |
+
# Initialize config manager first
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
|
| 73 |
self.config_manager.load_env()
|
| 74 |
|
| 75 |
+
# Initialize directories
|
| 76 |
+
self._initialize_directories()
|
| 77 |
+
|
| 78 |
+
# Check if there are CSPR files in the current database path
|
| 79 |
+
current_db_path = self.config_manager.get_env_value('CSPR_DB', '')
|
| 80 |
+
if current_db_path:
|
| 81 |
+
try:
|
| 82 |
+
import glob, os
|
| 83 |
+
cspr_files = glob.glob(os.path.join(current_db_path, "*.cspr"))
|
| 84 |
+
if not cspr_files:
|
| 85 |
+
# No CSPR files found, but keep the path
|
| 86 |
+
self.logger.info(f"No CSPR files found in {current_db_path}, but keeping the path")
|
| 87 |
+
# Only set first time startup to TRUE if there was no previous path
|
| 88 |
+
if not self.config_manager.get_env_value('CSPR_DB', ''):
|
| 89 |
+
self.config_manager.set_env_value('FIRST_TIME_START', 'TRUE')
|
| 90 |
+
else:
|
| 91 |
+
current_value = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE')
|
| 92 |
+
if current_value.upper() != 'FALSE':
|
| 93 |
+
self.config_manager.set_env_value('FIRST_TIME_START', 'FALSE')
|
| 94 |
+
except Exception as e:
|
| 95 |
+
self.logger.error(f"Error checking CSPR files: {str(e)}")
|
| 96 |
+
# Keep the path even on error
|
| 97 |
+
self.logger.info(f"Keeping database path despite error: {current_db_path}")
|
| 98 |
+
|
| 99 |
+
# Set first time startup flag
|
| 100 |
self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
|
| 101 |
|
| 102 |
+
# Initialize database manager after potential path reset
|
|
|
|
| 103 |
self._init_db_manager()
|
| 104 |
|
| 105 |
+
# Only preload essential controllers after determining startup state
|
| 106 |
+
self._preload_essential_controllers()
|
| 107 |
+
|
| 108 |
+
# Start background loading of commonly used modules
|
| 109 |
+
self._background_load_common_modules()
|
| 110 |
+
|
| 111 |
# Defer theme initialization
|
| 112 |
self._init_theme_settings()
|
| 113 |
|
|
|
|
| 195 |
|
| 196 |
def validate_db_path(self, path):
|
| 197 |
"""Validate the given database path"""
|
| 198 |
+
print("path", path)
|
| 199 |
+
print("db.manager validate_db_path", self.db_manager.validate_db_path(path))
|
| 200 |
return self.db_manager.validate_db_path(path)
|
| 201 |
|
| 202 |
def save_db_path(self, path):
|
|
|
|
| 322 |
def _get_window_class(self, window_name):
|
| 323 |
"""Get the controller class with optimized loading"""
|
| 324 |
try:
|
|
|
|
|
|
|
| 325 |
# Check if module is already cached
|
| 326 |
module_path = f"controllers.{window_name}Controller"
|
| 327 |
if module_path in self._module_cache:
|
|
|
|
| 354 |
if not hasattr(controller_module, class_name):
|
| 355 |
raise AttributeError(f"Controller module does not contain class {class_name}")
|
| 356 |
|
|
|
|
| 357 |
return getattr(controller_module, class_name)
|
| 358 |
|
| 359 |
except Exception as e:
|
|
|
|
| 374 |
self.logger.error(f"Error creating window {window_name}: {str(e)}")
|
| 375 |
raise
|
| 376 |
|
| 377 |
+
def get_startup_window(self, keep_db_path=False):
|
| 378 |
+
"""
|
| 379 |
+
Creates and returns a new startup window controller
|
| 380 |
+
|
| 381 |
+
Args:
|
| 382 |
+
keep_db_path (bool): If True, keeps the existing DB path when initializing startup
|
| 383 |
+
"""
|
| 384 |
+
from controllers.StartupWindowController import StartupWindowController
|
| 385 |
+
return StartupWindowController(self, keep_db_path=keep_db_path)
|
| 386 |
|
| 387 |
def get_home_window(self):
|
| 388 |
"""Get or create home window with proper initialization"""
|
|
|
|
| 419 |
def _background_load_common_modules(self):
|
| 420 |
"""Start background loading of commonly used modules"""
|
| 421 |
try:
|
| 422 |
+
common_modules = [
|
| 423 |
+
"MultitargetingWindow",
|
| 424 |
+
"PopulationAnalysisWindow",
|
| 425 |
+
"NewGenomeWindow",
|
| 426 |
+
"NewEndonuclease",
|
| 427 |
+
"NCBIWindow"
|
| 428 |
+
]
|
| 429 |
for module_name in common_modules:
|
| 430 |
if (module_name not in self._module_cache and
|
| 431 |
module_name not in self._preloading_modules):
|
|
|
|
| 445 |
preloader = self._preloading_modules[module_name]
|
| 446 |
if not preloader.isRunning(): # Only remove if thread is finished
|
| 447 |
del self._preloading_modules[module_name]
|
|
|
|
| 448 |
except Exception as e:
|
| 449 |
self.logger.error(f"Error handling preloaded module: {str(e)}")
|
| 450 |
|
|
|
|
| 534 |
def set_current_annotation_file(self, annotation_file):
|
| 535 |
"""Set the current annotation file and notify listeners"""
|
| 536 |
try:
|
|
|
|
|
|
|
|
|
|
| 537 |
if self._current_annotation_file != annotation_file:
|
| 538 |
self._current_annotation_file = annotation_file
|
|
|
|
| 539 |
self.annotation_file_changed.emit(annotation_file)
|
| 540 |
except Exception as e:
|
| 541 |
self.logger.error(f"Error setting current annotation file: {str(e)}")
|
| 542 |
|
| 543 |
def get_current_annotation_file(self):
|
| 544 |
"""Get the currently selected annotation file"""
|
| 545 |
+
return self._current_annotation_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
def get_scoring_options_window(self, view_targets_controller):
|
| 548 |
"""Create and return ScoringOptionsController instance"""
|
|
|
|
| 594 |
self.logger.error(f"Error adjusting path: {str(e)}")
|
| 595 |
return path # Return original path if adjustment fails
|
| 596 |
|
| 597 |
+
def get_stylesheet(self):
|
| 598 |
+
"""Get the current theme's stylesheet"""
|
| 599 |
+
current_theme = self.get_theme()
|
| 600 |
+
return self.get_dark_stylesheet() if current_theme == "dark" else self.get_light_stylesheet()
|
| 601 |
+
|
| 602 |
+
def get_dark_stylesheet(self):
|
| 603 |
+
"""Get dark theme stylesheet"""
|
| 604 |
+
theme = {
|
| 605 |
+
"bg_color": "#2b2b2b",
|
| 606 |
+
"fg_color": "#ffffff",
|
| 607 |
+
"button_bg_color": "#3a3a3a",
|
| 608 |
+
"button_border_color": "#5a5a5a",
|
| 609 |
+
"button_hover_bg_color": "#4a4a4a",
|
| 610 |
+
"input_bg_color": "#3a3a3a",
|
| 611 |
+
"input_border_color": "#5a5a5a",
|
| 612 |
+
"progress_bar_bg": "#3a3a3a",
|
| 613 |
+
"progress_bar_chunk": "#51b85e"
|
| 614 |
+
}
|
| 615 |
+
return self._get_themed_stylesheet(theme)
|
| 616 |
+
|
| 617 |
+
def get_light_stylesheet(self):
|
| 618 |
+
"""Get light theme stylesheet"""
|
| 619 |
+
theme = {
|
| 620 |
+
"bg_color": "#f0f0f0",
|
| 621 |
+
"fg_color": "#000000",
|
| 622 |
+
"button_bg_color": "#e0e0e0",
|
| 623 |
+
"button_border_color": "#c0c0c0",
|
| 624 |
+
"button_hover_bg_color": "#d0d0d0",
|
| 625 |
+
"input_bg_color": "#ffffff",
|
| 626 |
+
"input_border_color": "#c0c0c0",
|
| 627 |
+
"progress_bar_bg": "#e0e0e0",
|
| 628 |
+
"progress_bar_chunk": "#51b85e"
|
| 629 |
+
}
|
| 630 |
+
return self._get_themed_stylesheet(theme)
|
| 631 |
+
|
| 632 |
+
def _get_themed_stylesheet(self, theme):
|
| 633 |
+
"""Generate stylesheet based on theme colors"""
|
| 634 |
+
return f"""
|
| 635 |
+
QMainWindow, QWidget {{
|
| 636 |
+
background-color: {theme['bg_color']};
|
| 637 |
+
color: {theme['fg_color']};
|
| 638 |
+
}}
|
| 639 |
+
QPushButton {{
|
| 640 |
+
background-color: {theme['button_bg_color']};
|
| 641 |
+
border: 1px solid {theme['button_border_color']};
|
| 642 |
+
padding: 5px;
|
| 643 |
+
min-width: 80px;
|
| 644 |
+
}}
|
| 645 |
+
QPushButton:hover {{
|
| 646 |
+
background-color: {theme['button_hover_bg_color']};
|
| 647 |
+
}}
|
| 648 |
+
QLineEdit {{
|
| 649 |
+
background-color: {theme['input_bg_color']};
|
| 650 |
+
border: 1px solid {theme['input_border_color']};
|
| 651 |
+
padding: 5px;
|
| 652 |
+
}}
|
| 653 |
+
QComboBox {{
|
| 654 |
+
background-color: {theme['input_bg_color']};
|
| 655 |
+
border: 1px solid {theme['input_border_color']};
|
| 656 |
+
padding: 5px;
|
| 657 |
+
}}
|
| 658 |
+
QComboBox:hover {{
|
| 659 |
+
background-color: {theme['button_hover_bg_color']};
|
| 660 |
+
}}
|
| 661 |
+
QRadioButton {{
|
| 662 |
+
color: {theme['fg_color']};
|
| 663 |
+
}}
|
| 664 |
+
QProgressBar {{
|
| 665 |
+
border: 1px solid {theme['button_border_color']};
|
| 666 |
+
background-color: {theme['progress_bar_bg']};
|
| 667 |
+
text-align: center;
|
| 668 |
+
}}
|
| 669 |
+
QProgressBar::chunk {{
|
| 670 |
+
background-color: {theme['progress_bar_chunk']};
|
| 671 |
+
}}
|
| 672 |
+
QGroupBox {{
|
| 673 |
+
border: 1px solid {theme['button_border_color']};
|
| 674 |
+
margin-top: 0.5em;
|
| 675 |
+
padding-top: 0.5em;
|
| 676 |
+
}}
|
| 677 |
+
QGroupBox::title {{
|
| 678 |
+
color: {theme['fg_color']};
|
| 679 |
+
subcontrol-origin: margin;
|
| 680 |
+
left: 10px;
|
| 681 |
+
padding: 0 3px 0 3px;
|
| 682 |
+
}}
|
| 683 |
+
QDoubleSpinBox {{
|
| 684 |
+
background-color: {theme['input_bg_color']};
|
| 685 |
+
border: 1px solid {theme['input_border_color']};
|
| 686 |
+
padding: 5px;
|
| 687 |
+
}}
|
| 688 |
+
"""
|
| 689 |
+
|
| 690 |
+
def get_organism_files(self):
|
| 691 |
+
"""Get mapping of organisms to their files from database manager"""
|
| 692 |
+
organism_files, _ = self.db_manager.get_organisms_and_endos()
|
| 693 |
+
return organism_files
|
| 694 |
+
|
| 695 |
+
def get_groupbox_style(self) -> str:
|
| 696 |
+
"""Get the standardized groupbox style with green accent color"""
|
| 697 |
+
return """
|
| 698 |
+
QGroupBox:title {
|
| 699 |
+
subcontrol-origin: margin;
|
| 700 |
+
left: 10px;
|
| 701 |
+
padding: 0 5px 0 5px;
|
| 702 |
+
}
|
| 703 |
+
QGroupBox {
|
| 704 |
+
border: 2px solid rgb(111,181,110);
|
| 705 |
+
border-radius: 9px;
|
| 706 |
+
margin-top: 10px;
|
| 707 |
+
font: bold 14pt 'Arial';
|
| 708 |
+
}
|
| 709 |
+
QGroupBox#grpNavigationMenu {
|
| 710 |
+
border: 2px dashed rgb(88,89,91);
|
| 711 |
+
border-radius: 9px;
|
| 712 |
+
margin-top: 10px;
|
| 713 |
+
font: bold 14pt 'Arial';
|
| 714 |
+
}
|
| 715 |
+
"""
|
| 716 |
+
|
| 717 |
# Global instance
|
| 718 |
global_settings = None
|
|
@@ -5,15 +5,16 @@ from utils.ui import show_error
|
|
| 5 |
from models.DatabaseManager import FileChangeType
|
| 6 |
|
| 7 |
class HomeWindowModel:
|
| 8 |
-
def __init__(self, global_settings):
|
| 9 |
self.global_settings = global_settings
|
| 10 |
self.logger = global_settings.get_logger()
|
| 11 |
-
self.
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
def load_data(self) -> None:
|
| 19 |
"""Load all required data"""
|
|
@@ -56,8 +57,8 @@ class HomeWindowModel:
|
|
| 56 |
"""Load organism and endonuclease data from CSPR files"""
|
| 57 |
try:
|
| 58 |
# Clear existing data
|
| 59 |
-
self.
|
| 60 |
-
self.
|
| 61 |
|
| 62 |
cspr_files = glob.glob(os.path.join(self.global_settings.get_db_path(), "*.cspr"))
|
| 63 |
|
|
@@ -70,20 +71,20 @@ class HomeWindowModel:
|
|
| 70 |
organism = f.readline().strip().replace("GENOME: ", '')
|
| 71 |
|
| 72 |
# Update organism to files mapping
|
| 73 |
-
if organism not in self.
|
| 74 |
-
self.
|
| 75 |
-
self.
|
| 76 |
file_name,
|
| 77 |
file_name.replace(".cspr", "_repeats.db")
|
| 78 |
]
|
| 79 |
|
| 80 |
# Update organism to endonuclease mapping
|
| 81 |
-
if organism not in self.
|
| 82 |
-
self.
|
| 83 |
-
if endonuclease not in self.
|
| 84 |
-
self.
|
| 85 |
|
| 86 |
-
self.logger.debug(f"Loaded data for {len(self.
|
| 87 |
|
| 88 |
except Exception as e:
|
| 89 |
self.logger.error(f"Error loading organisms and endonucleases: {str(e)}")
|
|
@@ -99,12 +100,12 @@ class HomeWindowModel:
|
|
| 99 |
)
|
| 100 |
|
| 101 |
# Process files
|
| 102 |
-
self.
|
| 103 |
os.path.basename(file) for file in annotation_files
|
| 104 |
if not file.endswith('.index') # Exclude index files
|
| 105 |
}
|
| 106 |
|
| 107 |
-
self.logger.debug(f"Loaded {len(self.
|
| 108 |
|
| 109 |
except Exception as e:
|
| 110 |
self.logger.error(f"Error loading annotation files: {str(e)}")
|
|
@@ -112,15 +113,15 @@ class HomeWindowModel:
|
|
| 112 |
|
| 113 |
def get_organism_to_files(self) -> Dict[str, Dict[str, List[str]]]:
|
| 114 |
"""Get mapping of organisms to their files"""
|
| 115 |
-
return self.
|
| 116 |
|
| 117 |
def get_organism_to_endonuclease(self) -> Dict[str, List[str]]:
|
| 118 |
"""Get mapping of organisms to their endonucleases"""
|
| 119 |
-
return self.
|
| 120 |
|
| 121 |
def get_annotation_files(self) -> List[str]:
|
| 122 |
"""Get list of annotation files"""
|
| 123 |
-
return sorted(self.
|
| 124 |
|
| 125 |
def find_targets(self, input_data: dict) -> None:
|
| 126 |
pass
|
|
|
|
| 5 |
from models.DatabaseManager import FileChangeType
|
| 6 |
|
| 7 |
class HomeWindowModel:
|
| 8 |
+
def __init__(self, global_settings, skip_initial_load=False):
|
| 9 |
self.global_settings = global_settings
|
| 10 |
self.logger = global_settings.get_logger()
|
| 11 |
+
self._organism_to_files = {}
|
| 12 |
+
self._organism_to_endonuclease = {}
|
| 13 |
+
self._annotation_files = []
|
| 14 |
+
|
| 15 |
+
# Only load data if not skipped
|
| 16 |
+
if not skip_initial_load:
|
| 17 |
+
self.load_data()
|
| 18 |
|
| 19 |
def load_data(self) -> None:
|
| 20 |
"""Load all required data"""
|
|
|
|
| 57 |
"""Load organism and endonuclease data from CSPR files"""
|
| 58 |
try:
|
| 59 |
# Clear existing data
|
| 60 |
+
self._organism_to_files = {}
|
| 61 |
+
self._organism_to_endonuclease = {}
|
| 62 |
|
| 63 |
cspr_files = glob.glob(os.path.join(self.global_settings.get_db_path(), "*.cspr"))
|
| 64 |
|
|
|
|
| 71 |
organism = f.readline().strip().replace("GENOME: ", '')
|
| 72 |
|
| 73 |
# Update organism to files mapping
|
| 74 |
+
if organism not in self._organism_to_files:
|
| 75 |
+
self._organism_to_files[organism] = {}
|
| 76 |
+
self._organism_to_files[organism][endonuclease] = [
|
| 77 |
file_name,
|
| 78 |
file_name.replace(".cspr", "_repeats.db")
|
| 79 |
]
|
| 80 |
|
| 81 |
# Update organism to endonuclease mapping
|
| 82 |
+
if organism not in self._organism_to_endonuclease:
|
| 83 |
+
self._organism_to_endonuclease[organism] = []
|
| 84 |
+
if endonuclease not in self._organism_to_endonuclease[organism]:
|
| 85 |
+
self._organism_to_endonuclease[organism].append(endonuclease)
|
| 86 |
|
| 87 |
+
self.logger.debug(f"Loaded data for {len(self._organism_to_files)} organisms")
|
| 88 |
|
| 89 |
except Exception as e:
|
| 90 |
self.logger.error(f"Error loading organisms and endonucleases: {str(e)}")
|
|
|
|
| 100 |
)
|
| 101 |
|
| 102 |
# Process files
|
| 103 |
+
self._annotation_files = {
|
| 104 |
os.path.basename(file) for file in annotation_files
|
| 105 |
if not file.endswith('.index') # Exclude index files
|
| 106 |
}
|
| 107 |
|
| 108 |
+
self.logger.debug(f"Loaded {len(self._annotation_files)} annotation files")
|
| 109 |
|
| 110 |
except Exception as e:
|
| 111 |
self.logger.error(f"Error loading annotation files: {str(e)}")
|
|
|
|
| 113 |
|
| 114 |
def get_organism_to_files(self) -> Dict[str, Dict[str, List[str]]]:
|
| 115 |
"""Get mapping of organisms to their files"""
|
| 116 |
+
return self._organism_to_files
|
| 117 |
|
| 118 |
def get_organism_to_endonuclease(self) -> Dict[str, List[str]]:
|
| 119 |
"""Get mapping of organisms to their endonucleases"""
|
| 120 |
+
return self._organism_to_endonuclease
|
| 121 |
|
| 122 |
def get_annotation_files(self) -> List[str]:
|
| 123 |
"""Get list of annotation files"""
|
| 124 |
+
return sorted(self._annotation_files, key=str.lower)
|
| 125 |
|
| 126 |
def find_targets(self, input_data: dict) -> None:
|
| 127 |
pass
|
|
@@ -8,6 +8,7 @@ import os
|
|
| 8 |
import platform
|
| 9 |
import requests
|
| 10 |
from urllib.parse import urlparse
|
|
|
|
| 11 |
|
| 12 |
class NCBIWindowModel:
|
| 13 |
class DownloadThread(QtCore.QThread):
|
|
@@ -26,7 +27,8 @@ class NCBIWindowModel:
|
|
| 26 |
self.download_fna = download_fna
|
| 27 |
self.download_gbff = download_gbff
|
| 28 |
self.logger = controller.settings.get_logger()
|
| 29 |
-
self.db_path = controller.settings.
|
|
|
|
| 30 |
|
| 31 |
def run(self):
|
| 32 |
try:
|
|
@@ -54,7 +56,7 @@ class NCBIWindowModel:
|
|
| 54 |
file_type = 'FNA' if is_fna else 'GBFF'
|
| 55 |
extension = '.gz' if is_gzipped else ''
|
| 56 |
|
| 57 |
-
# Create output directory using the
|
| 58 |
output_dir = os.path.join(self.db_path, file_type)
|
| 59 |
os.makedirs(output_dir, exist_ok=True)
|
| 60 |
|
|
@@ -129,6 +131,7 @@ class NCBIWindowModel:
|
|
| 129 |
file_type = 'FNA' if is_fna else 'GBFF'
|
| 130 |
extension = '.gz' if is_gzipped else ''
|
| 131 |
|
|
|
|
| 132 |
local_filename = os.path.join(
|
| 133 |
self.db_path,
|
| 134 |
file_type,
|
|
@@ -185,6 +188,9 @@ class NCBIWindowModel:
|
|
| 185 |
self.controller.model.add_downloaded_file(decompressed_filename)
|
| 186 |
|
| 187 |
def __init__(self, settings):
|
|
|
|
|
|
|
|
|
|
| 188 |
self.settings = settings
|
| 189 |
self.logger = settings.get_logger()
|
| 190 |
self.df = pd.DataFrame()
|
|
@@ -202,6 +208,8 @@ class NCBIWindowModel:
|
|
| 202 |
"ENA (European Nucleotide Archive)": self._search_ena,
|
| 203 |
"UCSC Genome Browser": self._search_ucsc
|
| 204 |
}
|
|
|
|
|
|
|
| 205 |
|
| 206 |
def search_ncbi(self, search_params):
|
| 207 |
"""Search selected database with given parameters"""
|
|
@@ -628,9 +636,20 @@ class NCBIWindowModel:
|
|
| 628 |
raise
|
| 629 |
|
| 630 |
def get_output_path(self, file_type):
|
| 631 |
-
|
|
|
|
|
|
|
| 632 |
self.logger.debug(f"Using database path for downloads: {db_path}")
|
| 633 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
|
| 635 |
def rename_file(self, old_name, new_name, file_type):
|
| 636 |
old_path = os.path.join(self.get_output_path(file_type), old_name)
|
|
|
|
| 8 |
import platform
|
| 9 |
import requests
|
| 10 |
from urllib.parse import urlparse
|
| 11 |
+
import time
|
| 12 |
|
| 13 |
class NCBIWindowModel:
|
| 14 |
class DownloadThread(QtCore.QThread):
|
|
|
|
| 27 |
self.download_fna = download_fna
|
| 28 |
self.download_gbff = download_gbff
|
| 29 |
self.logger = controller.settings.get_logger()
|
| 30 |
+
self.db_path = controller.settings.db_manager.get_active_db_path()
|
| 31 |
+
self.logger.debug(f"Using database path for downloads: {self.db_path}")
|
| 32 |
|
| 33 |
def run(self):
|
| 34 |
try:
|
|
|
|
| 56 |
file_type = 'FNA' if is_fna else 'GBFF'
|
| 57 |
extension = '.gz' if is_gzipped else ''
|
| 58 |
|
| 59 |
+
# Create output directory using the active database path
|
| 60 |
output_dir = os.path.join(self.db_path, file_type)
|
| 61 |
os.makedirs(output_dir, exist_ok=True)
|
| 62 |
|
|
|
|
| 131 |
file_type = 'FNA' if is_fna else 'GBFF'
|
| 132 |
extension = '.gz' if is_gzipped else ''
|
| 133 |
|
| 134 |
+
# Use the active database path for FTP downloads as well
|
| 135 |
local_filename = os.path.join(
|
| 136 |
self.db_path,
|
| 137 |
file_type,
|
|
|
|
| 188 |
self.controller.model.add_downloaded_file(decompressed_filename)
|
| 189 |
|
| 190 |
def __init__(self, settings):
|
| 191 |
+
start_time = time.time()
|
| 192 |
+
settings.logger.debug("Starting NCBIWindowModel initialization")
|
| 193 |
+
|
| 194 |
self.settings = settings
|
| 195 |
self.logger = settings.get_logger()
|
| 196 |
self.df = pd.DataFrame()
|
|
|
|
| 208 |
"ENA (European Nucleotide Archive)": self._search_ena,
|
| 209 |
"UCSC Genome Browser": self._search_ucsc
|
| 210 |
}
|
| 211 |
+
|
| 212 |
+
self.logger.debug(f"NCBIWindowModel initialization took: {time.time() - start_time:.2f} seconds")
|
| 213 |
|
| 214 |
def search_ncbi(self, search_params):
|
| 215 |
"""Search selected database with given parameters"""
|
|
|
|
| 636 |
raise
|
| 637 |
|
| 638 |
def get_output_path(self, file_type):
|
| 639 |
+
"""Get the appropriate output path for downloads"""
|
| 640 |
+
# Use the active database path which includes pending path during new genome analysis
|
| 641 |
+
db_path = self.settings.db_manager.get_active_db_path()
|
| 642 |
self.logger.debug(f"Using database path for downloads: {db_path}")
|
| 643 |
+
output_path = os.path.join(db_path, file_type)
|
| 644 |
+
# Ensure the directory exists
|
| 645 |
+
os.makedirs(output_path, exist_ok=True)
|
| 646 |
+
return output_path
|
| 647 |
+
|
| 648 |
+
def cancel_pending_path(self):
|
| 649 |
+
"""Cancel any pending path changes"""
|
| 650 |
+
if self.settings.db_manager.pending_db_path:
|
| 651 |
+
self.logger.info("Cancelling pending database path change")
|
| 652 |
+
self.settings.db_manager.pending_db_path = None
|
| 653 |
|
| 654 |
def rename_file(self, old_name, new_name, file_type):
|
| 655 |
old_path = os.path.join(self.get_output_path(file_type), old_name)
|
|
@@ -64,13 +64,13 @@ class NewGenomeWindowModel(QObject):
|
|
| 64 |
return False
|
| 65 |
|
| 66 |
def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
|
| 67 |
-
db_path = self.settings.
|
| 68 |
|
| 69 |
# Ensure db_path ends with a forward slash
|
| 70 |
if not db_path.endswith('/'):
|
| 71 |
db_path = f"{db_path}/"
|
| 72 |
|
| 73 |
-
self.logger.debug(f"Using database path: {db_path}") # Add logging
|
| 74 |
|
| 75 |
print(f"The endonuclease data is {endonuclease_data}")
|
| 76 |
|
|
|
|
| 64 |
return False
|
| 65 |
|
| 66 |
def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
|
| 67 |
+
db_path = self.settings.db_manager.get_active_db_path()
|
| 68 |
|
| 69 |
# Ensure db_path ends with a forward slash
|
| 70 |
if not db_path.endswith('/'):
|
| 71 |
db_path = f"{db_path}/"
|
| 72 |
|
| 73 |
+
self.logger.debug(f"Using database path for job processing: {db_path}") # Add logging
|
| 74 |
|
| 75 |
print(f"The endonuclease data is {endonuclease_data}")
|
| 76 |
|
|
@@ -1,29 +1 @@
|
|
| 1 |
-
|
| 2 |
-
CACTTATGACCGGGCAACTT:0.000000
|
| 3 |
-
ACACTTATGACCGGGCAACT:0.080598
|
| 4 |
-
0.080598,1,-100695,ACAATTACGCCCGGGCAACC
|
| 5 |
-
TCAAAATAGCCCAAGTTGCC:0.000000
|
| 6 |
-
ATTTTGCTACACTTATGACC:0.000000
|
| 7 |
-
AATTTTGCTACACTTATGAC:0.000000
|
| 8 |
-
GGGAATACTCCCTTTTATTG:0.000000
|
| 9 |
-
GCAAAATTATCCTCAATAAA:0.018309
|
| 10 |
-
0.018309,1,1937923,GCAAAGTTTTCCTCAATATT
|
| 11 |
-
CAAAATTATCCTCAATAAAA:0.107086
|
| 12 |
-
0.059761,1,2361124,CACATTTACCCTCAATGAAA
|
| 13 |
-
0.154411,1,4041223,CAAATCTATACTGAATAAAA
|
| 14 |
-
CAGCTACAACCCGTGGCGGA:0.000000
|
| 15 |
-
CCAGCTACAACCCGTGGCGG:0.106259
|
| 16 |
-
0.106259,1,4257161,ACAACTGCAAGCCGTGGCGG
|
| 17 |
-
CCGCCAGCTACAACCCGTGG:0.000000
|
| 18 |
-
AGGGAGTATTCCCTCCGCCA:0.000000
|
| 19 |
-
GACCCGCCAGCTACAACCCG:0.000000
|
| 20 |
-
GGGAGTATTCCCTCCGCCAC:0.000000
|
| 21 |
-
CCTCCGCCACGGGTTGTAGC:0.129554
|
| 22 |
-
0.129554,1,-165804,GTTACGCCACGGGTTGTAGA
|
| 23 |
-
CCGCCACGGGTTGTAGCTGG:0.000000
|
| 24 |
-
CGCCACGGGTTGTAGCTGGC:0.000000
|
| 25 |
-
GGACTACCAACGTTCACCAC:0.221431
|
| 26 |
-
0.234722,1,-469831,TCGCTACCATCGTTCACCAC
|
| 27 |
-
0.208141,1,2847166,GGAGAACCGACGGTCACCAC
|
| 28 |
-
AGATAGTGTTCGTAATCCAG:0.000000
|
| 29 |
-
CGTAATCCAGTGGTGAACGT:0.000000
|
|
|
|
| 1 |
+
AVG OUTPUT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -105,6 +105,7 @@ class OffTargetModel(QObject):
|
|
| 105 |
self.logger.debug(f"Max mismatches: {parameters['max_mismatches']}")
|
| 106 |
self.logger.debug(f"Tolerance: {parameters['tolerance']}")
|
| 107 |
self.logger.debug(f"Average output: {parameters['average_output']}")
|
|
|
|
| 108 |
|
| 109 |
# Set working directory
|
| 110 |
off_target_dir = self.global_settings.get_off_target_dir_path()
|
|
@@ -129,30 +130,8 @@ class OffTargetModel(QObject):
|
|
| 129 |
self.logger.debug("Starting QProcess with command:")
|
| 130 |
self.logger.debug(cmd)
|
| 131 |
|
| 132 |
-
|
| 133 |
-
example_cmd = [
|
| 134 |
-
"/Users/admin/Documents/proj/CASPERtest/CASPERapp/src/models/OffTarget/temp.txt",
|
| 135 |
-
"spCas9",
|
| 136 |
-
"/Users/admin/Documents/CASPERdb2/eck_12_spCas9.cspr",
|
| 137 |
-
"/Users/admin/Documents/CASPERdb2/eck_12_spCas9_repeats.db",
|
| 138 |
-
"/Users/admin/Documents/CASPERdb2/testtttt",
|
| 139 |
-
"/Users/admin/Documents/proj/CASPERtest/CASPERapp/config/CASPERinfo",
|
| 140 |
-
"4",
|
| 141 |
-
"0.05",
|
| 142 |
-
"FALSE",
|
| 143 |
-
"TRUE",
|
| 144 |
-
"MATRIX:HSU MATRIX-spCas9-2013"
|
| 145 |
-
]
|
| 146 |
-
|
| 147 |
-
print(f"cmd: {cmd}")
|
| 148 |
-
|
| 149 |
-
print(f"example_cmd: {example_cmd}")
|
| 150 |
-
|
| 151 |
-
|
| 152 |
self.process.start(str(program_path), cmd)
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
return True
|
| 157 |
|
| 158 |
except Exception as e:
|
|
@@ -331,6 +310,21 @@ class OffTargetModel(QObject):
|
|
| 331 |
casper_info_path = f'{self.global_settings.get_casper_info_path()}'
|
| 332 |
endo = f'{parameters["endonuclease"]}'
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
# Build command exactly as in old version
|
| 335 |
cmd_parts = [
|
| 336 |
temp_path,
|
|
@@ -343,7 +337,8 @@ class OffTargetModel(QObject):
|
|
| 343 |
str(parameters['tolerance']),
|
| 344 |
'FALSE' if parameters['average_output'] else 'TRUE',
|
| 345 |
'TRUE' if parameters['average_output'] else 'FALSE',
|
| 346 |
-
f'{self._get_hsu_value(parameters)}'
|
|
|
|
| 347 |
]
|
| 348 |
|
| 349 |
return program_path, cmd_parts
|
|
|
|
| 105 |
self.logger.debug(f"Max mismatches: {parameters['max_mismatches']}")
|
| 106 |
self.logger.debug(f"Tolerance: {parameters['tolerance']}")
|
| 107 |
self.logger.debug(f"Average output: {parameters['average_output']}")
|
| 108 |
+
self.logger.debug(f"Annotation file: {self.global_settings.get_current_annotation_file()}")
|
| 109 |
|
| 110 |
# Set working directory
|
| 111 |
off_target_dir = self.global_settings.get_off_target_dir_path()
|
|
|
|
| 130 |
self.logger.debug("Starting QProcess with command:")
|
| 131 |
self.logger.debug(cmd)
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
self.process.start(str(program_path), cmd)
|
| 134 |
|
|
|
|
|
|
|
| 135 |
return True
|
| 136 |
|
| 137 |
except Exception as e:
|
|
|
|
| 310 |
casper_info_path = f'{self.global_settings.get_casper_info_path()}'
|
| 311 |
endo = f'{parameters["endonuclease"]}'
|
| 312 |
|
| 313 |
+
# Get annotation file path
|
| 314 |
+
annotation_file = self.global_settings.get_current_annotation_file()
|
| 315 |
+
if not annotation_file:
|
| 316 |
+
raise ValueError("No annotation file selected")
|
| 317 |
+
|
| 318 |
+
# Build full annotation path and verify it exists
|
| 319 |
+
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 320 |
+
if not os.path.isfile(annotation_path):
|
| 321 |
+
# Try without GBFF subdirectory
|
| 322 |
+
annotation_path = os.path.join(self.global_settings.get_db_path(), annotation_file)
|
| 323 |
+
if not os.path.isfile(annotation_path):
|
| 324 |
+
raise ValueError(f"Annotation file not found at {annotation_path}")
|
| 325 |
+
|
| 326 |
+
self.logger.debug(f"Using annotation file: {annotation_path}")
|
| 327 |
+
|
| 328 |
# Build command exactly as in old version
|
| 329 |
cmd_parts = [
|
| 330 |
temp_path,
|
|
|
|
| 337 |
str(parameters['tolerance']),
|
| 338 |
'FALSE' if parameters['average_output'] else 'TRUE',
|
| 339 |
'TRUE' if parameters['average_output'] else 'FALSE',
|
| 340 |
+
f'{self._get_hsu_value(parameters)}',
|
| 341 |
+
annotation_path # Add annotation file path
|
| 342 |
]
|
| 343 |
|
| 344 |
return program_path, cmd_parts
|
|
@@ -19,11 +19,8 @@ class PopulationAnalysisWindowModel:
|
|
| 19 |
def load_endonucleases(self):
|
| 20 |
"""Load endonucleases from GlobalSettings"""
|
| 21 |
try:
|
| 22 |
-
self.logger.info("Starting load_endonucleases()")
|
| 23 |
-
|
| 24 |
# Get endonucleases from global settings
|
| 25 |
endos = self.settings.get_endonucleases()
|
| 26 |
-
self.logger.debug(f"Raw endonucleases from settings: {endos}")
|
| 27 |
|
| 28 |
if not endos:
|
| 29 |
self.logger.warning("No endonucleases returned from settings")
|
|
@@ -32,7 +29,6 @@ class PopulationAnalysisWindowModel:
|
|
| 32 |
# Format the endonucleases for display
|
| 33 |
formatted_endos = {}
|
| 34 |
for endo, data in endos.items():
|
| 35 |
-
self.logger.debug(f"Processing endo: {endo}, data: {data}")
|
| 36 |
pam = data.get('pam', '').strip()
|
| 37 |
# Remove any extra "PAM:" text that might be in the PAM string
|
| 38 |
pam = pam.replace('PAM:', '').strip()
|
|
@@ -44,7 +40,6 @@ class PopulationAnalysisWindowModel:
|
|
| 44 |
data.get('default_seed_length', ''),
|
| 45 |
data.get('default_three_length', ''))
|
| 46 |
|
| 47 |
-
self.logger.info(f"Successfully formatted {len(formatted_endos)} endonucleases")
|
| 48 |
self.logger.debug(f"Formatted endonucleases: {formatted_endos}")
|
| 49 |
return formatted_endos
|
| 50 |
|
|
|
|
| 19 |
def load_endonucleases(self):
|
| 20 |
"""Load endonucleases from GlobalSettings"""
|
| 21 |
try:
|
|
|
|
|
|
|
| 22 |
# Get endonucleases from global settings
|
| 23 |
endos = self.settings.get_endonucleases()
|
|
|
|
| 24 |
|
| 25 |
if not endos:
|
| 26 |
self.logger.warning("No endonucleases returned from settings")
|
|
|
|
| 29 |
# Format the endonucleases for display
|
| 30 |
formatted_endos = {}
|
| 31 |
for endo, data in endos.items():
|
|
|
|
| 32 |
pam = data.get('pam', '').strip()
|
| 33 |
# Remove any extra "PAM:" text that might be in the PAM string
|
| 34 |
pam = pam.replace('PAM:', '').strip()
|
|
|
|
| 40 |
data.get('default_seed_length', ''),
|
| 41 |
data.get('default_three_length', ''))
|
| 42 |
|
|
|
|
| 43 |
self.logger.debug(f"Formatted endonucleases: {formatted_endos}")
|
| 44 |
return formatted_endos
|
| 45 |
|
|
@@ -2,6 +2,7 @@ from PyQt6.QtCore import QObject, pyqtSignal
|
|
| 2 |
|
| 3 |
class StartupWindowModel(QObject):
|
| 4 |
db_state_updated = pyqtSignal(bool, str, list)
|
|
|
|
| 5 |
|
| 6 |
def __init__(self, global_settings):
|
| 7 |
super().__init__()
|
|
@@ -12,16 +13,32 @@ class StartupWindowModel(QObject):
|
|
| 12 |
self.settings.db_manager.db_state_changed.connect(self.on_db_state_updated)
|
| 13 |
|
| 14 |
def get_db_path(self):
|
|
|
|
| 15 |
return self.settings.get_db_path()
|
| 16 |
|
| 17 |
-
def save_db_path(self,
|
| 18 |
-
"""Save the database path
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
def on_db_state_updated(self, is_valid, message, cspr_files):
|
| 25 |
"""Handle database state updates"""
|
| 26 |
-
self.
|
|
|
|
|
|
|
|
|
|
| 27 |
self.db_state_updated.emit(is_valid, message, cspr_files)
|
|
|
|
| 2 |
|
| 3 |
class StartupWindowModel(QObject):
|
| 4 |
db_state_updated = pyqtSignal(bool, str, list)
|
| 5 |
+
_is_saving = False # Add flag to prevent recursion
|
| 6 |
|
| 7 |
def __init__(self, global_settings):
|
| 8 |
super().__init__()
|
|
|
|
| 13 |
self.settings.db_manager.db_state_changed.connect(self.on_db_state_updated)
|
| 14 |
|
| 15 |
def get_db_path(self):
|
| 16 |
+
"""Get the current database path without modifying it"""
|
| 17 |
return self.settings.get_db_path()
|
| 18 |
|
| 19 |
+
def save_db_path(self, path):
|
| 20 |
+
"""Save the database path"""
|
| 21 |
+
try:
|
| 22 |
+
if self._is_saving: # Prevent recursive saves
|
| 23 |
+
return
|
| 24 |
+
|
| 25 |
+
self._is_saving = True
|
| 26 |
+
try:
|
| 27 |
+
# Don't clear the path if it's invalid - let the controller handle that
|
| 28 |
+
self.settings.save_db_path(path)
|
| 29 |
+
self.settings.update_db_state()
|
| 30 |
+
finally:
|
| 31 |
+
self._is_saving = False
|
| 32 |
+
|
| 33 |
+
except Exception as e:
|
| 34 |
+
self._is_saving = False
|
| 35 |
+
self.logger.error(f"Error saving database path: {str(e)}")
|
| 36 |
+
raise
|
| 37 |
|
| 38 |
def on_db_state_updated(self, is_valid, message, cspr_files):
|
| 39 |
"""Handle database state updates"""
|
| 40 |
+
if self._is_saving: # Don't emit signals during save operation
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
self.logger.debug(f"StartupWindowModel received db state update: valid={is_valid}, message={message}")
|
| 44 |
self.db_state_updated.emit(is_valid, message, cspr_files)
|
|
@@ -1,88 +1,67 @@
|
|
| 1 |
from models.CSPRparser import CSPRparser
|
| 2 |
-
from models.
|
| 3 |
-
from models.AnnotationParser import AnnotationParser
|
| 4 |
-
import os
|
| 5 |
from Bio import SeqIO
|
| 6 |
from collections import defaultdict
|
| 7 |
import traceback
|
|
|
|
| 8 |
|
| 9 |
-
class ViewTargetsModel(
|
| 10 |
def __init__(self, global_settings):
|
| 11 |
super().__init__(global_settings)
|
|
|
|
|
|
|
| 12 |
self.guides = []
|
| 13 |
self.cspr_parser = None
|
| 14 |
-
self.annotation_parser = None
|
| 15 |
self.gene_sequence = ""
|
| 16 |
self.highlighted_sequence = ""
|
| 17 |
self.gene_info = {}
|
| 18 |
self.available_genes = []
|
| 19 |
self.filter_options = {}
|
| 20 |
self.scoring_options = {}
|
| 21 |
-
self.annotation_path = ""
|
| 22 |
self.current_gene_start = 0
|
| 23 |
self.current_gene_end = 0
|
| 24 |
self.extended_sequence = ""
|
| 25 |
self.chromosome = ""
|
| 26 |
|
|
|
|
| 27 |
self._gene_data_cache = {}
|
| 28 |
self._sequence_cache = {}
|
| 29 |
self._parser_cache = {}
|
| 30 |
self._chromosome_seqs = {}
|
| 31 |
self._cached_guides = {}
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
#
|
| 36 |
-
self.
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
)
|
| 41 |
-
self.logger.debug(f"Initialized annotation path: {self.annotation_path}")
|
| 42 |
-
|
| 43 |
-
def cleanup(self):
|
| 44 |
-
"""Cleanup method to be called when the view is closed"""
|
| 45 |
-
try:
|
| 46 |
-
# Disconnect from annotation file changes
|
| 47 |
-
if hasattr(self, '_annotation_signal'):
|
| 48 |
-
self.global_settings.annotation_file_changed.disconnect(self._on_annotation_file_changed)
|
| 49 |
-
self.global_settings.logger.debug("ViewTargetsModel disconnected from annotation file changes")
|
| 50 |
-
|
| 51 |
-
self._gene_data_cache.clear()
|
| 52 |
-
self._sequence_cache.clear()
|
| 53 |
-
self._parser_cache.clear()
|
| 54 |
-
|
| 55 |
-
except Exception as e:
|
| 56 |
-
self.global_settings.logger.error(f"Error in ViewTargetsModel cleanup: {str(e)}")
|
| 57 |
|
| 58 |
-
def
|
| 59 |
-
"""
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
self.gene_sequence = ""
|
| 73 |
-
self.highlighted_sequence = ""
|
| 74 |
-
self.gene_info = {}
|
| 75 |
-
self.available_genes = []
|
| 76 |
-
self._chromosome_seqs = {}
|
| 77 |
-
|
| 78 |
-
except Exception as e:
|
| 79 |
-
self.logger.error(f"Error in _on_annotation_file_changed: {str(e)}")
|
| 80 |
|
| 81 |
def load_guides(self, selected_targets, organism, endonuclease):
|
| 82 |
"""Load guides with proper error handling"""
|
| 83 |
try:
|
| 84 |
-
self.logger.debug(f"Starting load_guides with {len(selected_targets)} targets")
|
| 85 |
-
|
| 86 |
self.organism = organism
|
| 87 |
self.endonuclease = endonuclease
|
| 88 |
|
|
@@ -92,7 +71,7 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 92 |
self.cspr_parser = self._parser_cache[cspr_key]
|
| 93 |
self.logger.debug("Using cached CSPR parser")
|
| 94 |
else:
|
| 95 |
-
org_files = self.
|
| 96 |
if organism not in org_files or endonuclease not in org_files[organism]:
|
| 97 |
self.logger.error(f"No CSPR file found for {organism} and {endonuclease}")
|
| 98 |
return
|
|
@@ -101,7 +80,6 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 101 |
cspr_path = os.path.join(self.global_settings.get_db_path(), cspr_file)
|
| 102 |
self.cspr_parser = CSPRparser(cspr_path, self.global_settings.get_casper_info_path())
|
| 103 |
self._parser_cache[cspr_key] = self.cspr_parser
|
| 104 |
-
self.logger.debug("Created new CSPR parser")
|
| 105 |
|
| 106 |
# Initialize guides and genes
|
| 107 |
self.guides = []
|
|
@@ -156,8 +134,6 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 156 |
self.guides = list(unique_guides.values())
|
| 157 |
|
| 158 |
self.logger.debug(f"Found {len(self.guides)} unique guides")
|
| 159 |
-
self.logger.debug(f"Available genes: {self.available_genes}")
|
| 160 |
-
|
| 161 |
except Exception as e:
|
| 162 |
self.logger.error(f"Error in load_guides: {str(e)}")
|
| 163 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
@@ -174,13 +150,6 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 174 |
|
| 175 |
return self._chromosome_seqs.get(chromosome)
|
| 176 |
|
| 177 |
-
def _initialize_annotation_parser(self):
|
| 178 |
-
"""Initialize annotation parser if not already initialized"""
|
| 179 |
-
if self.annotation_parser is None:
|
| 180 |
-
self.annotation_parser = AnnotationParser(self.global_settings)
|
| 181 |
-
if self.annotation_path:
|
| 182 |
-
self.annotation_parser.set_annotation_file(self.annotation_path)
|
| 183 |
-
|
| 184 |
def get_gene_data(self, locus_tag):
|
| 185 |
"""Get gene data with proper error handling"""
|
| 186 |
try:
|
|
@@ -192,25 +161,19 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 192 |
if locus_tag in self._gene_data_cache:
|
| 193 |
return self._gene_data_cache[locus_tag]
|
| 194 |
|
| 195 |
-
#
|
| 196 |
-
if not
|
| 197 |
-
|
| 198 |
-
annotation_file = self.global_settings.get_current_annotation_file()
|
| 199 |
-
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 200 |
-
self.annotation_parser.set_annotation_file(annotation_path)
|
| 201 |
-
self.logger.debug(f"Initialized annotation parser with file: {annotation_path}")
|
| 202 |
|
| 203 |
# Get gene data from parser with proper string conversion
|
| 204 |
gene_data = None
|
| 205 |
if isinstance(locus_tag, (str, int)):
|
| 206 |
locus_tag_str = str(locus_tag).strip()
|
| 207 |
-
self.logger.debug(f"Searching for locus tag: {locus_tag_str}")
|
| 208 |
# Look up by locus tag directly
|
| 209 |
gene_data = self.annotation_parser.get_gene_data(locus_tag_str.lower())
|
| 210 |
|
| 211 |
if gene_data:
|
| 212 |
self._gene_data_cache[locus_tag] = gene_data
|
| 213 |
-
self.logger.debug(f"Found gene data: {gene_data.keys()}")
|
| 214 |
else:
|
| 215 |
self.logger.debug(f"No gene data found for locus tag: {locus_tag}")
|
| 216 |
|
|
@@ -244,7 +207,6 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 244 |
self.logger.debug(f"View exons only is: {getattr(self, '_view_exons_only', False)}")
|
| 245 |
|
| 246 |
# Regular gene-based search
|
| 247 |
-
self.logger.debug(f"Getting gene data for locus tag: {identifier}")
|
| 248 |
gene_data = self.get_gene_data(identifier)
|
| 249 |
if not gene_data or 'info' not in gene_data:
|
| 250 |
self.logger.warning(f"No gene data found for locus tag: {identifier}")
|
|
@@ -255,53 +217,75 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 255 |
print(f"gene_data: {gene_data}")
|
| 256 |
full_location = gene_data['info'].get('full_location', '')
|
| 257 |
print(f"Full location: {full_location}")
|
| 258 |
-
if full_location
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
-
#
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
print(f"relative_start: {relative_start}, relative_end: {relative_end}")
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
# If not in exons-only mode or no exons to process, return normal sequence
|
| 302 |
if 'sequence' in gene_data:
|
| 303 |
sequence = gene_data['sequence']
|
| 304 |
-
self.logger.debug(f"Got sequence of length: {len(sequence)}")
|
| 305 |
|
| 306 |
# Format sequence with padding in lowercase (only if not in exons-only mode)
|
| 307 |
if not hasattr(self, '_view_exons_only') or not self._view_exons_only:
|
|
@@ -328,8 +312,6 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 328 |
'end': gene_data['info']['end'],
|
| 329 |
'full_location': gene_data['info'].get('full_location', '')
|
| 330 |
}
|
| 331 |
-
|
| 332 |
-
self.logger.debug(f"Returning sequence of length: {len(formatted_sequence)}")
|
| 333 |
return result
|
| 334 |
|
| 335 |
self.logger.warning(f"No sequence data found in gene_data for {identifier}")
|
|
@@ -343,27 +325,25 @@ class ViewTargetsModel(HomeWindowModel):
|
|
| 343 |
def _get_sequence_for_position(self, chrom, start, end):
|
| 344 |
"""Get sequence for a given position with proper padding handling"""
|
| 345 |
try:
|
| 346 |
-
if not
|
| 347 |
-
|
| 348 |
-
annotation_file = self.global_settings.get_current_annotation_file()
|
| 349 |
-
annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
|
| 350 |
-
self.annotation_parser.set_annotation_file(annotation_path)
|
| 351 |
|
| 352 |
feature_info = {
|
| 353 |
'chromosome': chrom, # Use raw chromosome ID directly
|
| 354 |
-
'start': start
|
| 355 |
'end': end
|
| 356 |
}
|
| 357 |
|
| 358 |
self.logger.debug(f"Getting sequence for feature info: {feature_info}")
|
| 359 |
|
| 360 |
-
|
|
|
|
| 361 |
if sequence:
|
| 362 |
padding = 30
|
| 363 |
|
| 364 |
# Handle start position padding
|
| 365 |
-
if start ==
|
| 366 |
-
# No padding at start if starting at position
|
| 367 |
five_prime_pad = ""
|
| 368 |
main_sequence = sequence[:-(padding if len(sequence) > padding else 0)].upper()
|
| 369 |
else:
|
|
|
|
| 1 |
from models.CSPRparser import CSPRparser
|
| 2 |
+
from models.BaseModel import BaseModel
|
|
|
|
|
|
|
| 3 |
from Bio import SeqIO
|
| 4 |
from collections import defaultdict
|
| 5 |
import traceback
|
| 6 |
+
import os
|
| 7 |
|
| 8 |
+
class ViewTargetsModel(BaseModel):
|
| 9 |
def __init__(self, global_settings):
|
| 10 |
super().__init__(global_settings)
|
| 11 |
+
|
| 12 |
+
# Initialize model state
|
| 13 |
self.guides = []
|
| 14 |
self.cspr_parser = None
|
|
|
|
| 15 |
self.gene_sequence = ""
|
| 16 |
self.highlighted_sequence = ""
|
| 17 |
self.gene_info = {}
|
| 18 |
self.available_genes = []
|
| 19 |
self.filter_options = {}
|
| 20 |
self.scoring_options = {}
|
|
|
|
| 21 |
self.current_gene_start = 0
|
| 22 |
self.current_gene_end = 0
|
| 23 |
self.extended_sequence = ""
|
| 24 |
self.chromosome = ""
|
| 25 |
|
| 26 |
+
# Initialize caches
|
| 27 |
self._gene_data_cache = {}
|
| 28 |
self._sequence_cache = {}
|
| 29 |
self._parser_cache = {}
|
| 30 |
self._chromosome_seqs = {}
|
| 31 |
self._cached_guides = {}
|
| 32 |
|
| 33 |
+
def _clear_caches(self):
|
| 34 |
+
"""Clear all model-specific caches"""
|
| 35 |
+
self._gene_data_cache.clear()
|
| 36 |
+
self._sequence_cache.clear()
|
| 37 |
+
self._parser_cache.clear()
|
| 38 |
+
self._chromosome_seqs.clear()
|
| 39 |
+
self._cached_guides.clear()
|
| 40 |
|
| 41 |
+
# Clear other stored data
|
| 42 |
+
self.gene_sequence = ""
|
| 43 |
+
self.highlighted_sequence = ""
|
| 44 |
+
self.gene_info = {}
|
| 45 |
+
self.available_genes = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
def _ensure_annotation_parser(self) -> bool:
|
| 48 |
+
"""Ensure annotation parser is initialized
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
bool: True if parser is ready, False otherwise
|
| 52 |
+
"""
|
| 53 |
+
if self.annotation_parser is None:
|
| 54 |
+
try:
|
| 55 |
+
self._initialize_annotation_parser()
|
| 56 |
+
return True
|
| 57 |
+
except Exception as e:
|
| 58 |
+
self.logger.error(f"Failed to initialize annotation parser: {str(e)}")
|
| 59 |
+
return False
|
| 60 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
def load_guides(self, selected_targets, organism, endonuclease):
|
| 63 |
"""Load guides with proper error handling"""
|
| 64 |
try:
|
|
|
|
|
|
|
| 65 |
self.organism = organism
|
| 66 |
self.endonuclease = endonuclease
|
| 67 |
|
|
|
|
| 71 |
self.cspr_parser = self._parser_cache[cspr_key]
|
| 72 |
self.logger.debug("Using cached CSPR parser")
|
| 73 |
else:
|
| 74 |
+
org_files = self.global_settings.get_organism_files()
|
| 75 |
if organism not in org_files or endonuclease not in org_files[organism]:
|
| 76 |
self.logger.error(f"No CSPR file found for {organism} and {endonuclease}")
|
| 77 |
return
|
|
|
|
| 80 |
cspr_path = os.path.join(self.global_settings.get_db_path(), cspr_file)
|
| 81 |
self.cspr_parser = CSPRparser(cspr_path, self.global_settings.get_casper_info_path())
|
| 82 |
self._parser_cache[cspr_key] = self.cspr_parser
|
|
|
|
| 83 |
|
| 84 |
# Initialize guides and genes
|
| 85 |
self.guides = []
|
|
|
|
| 134 |
self.guides = list(unique_guides.values())
|
| 135 |
|
| 136 |
self.logger.debug(f"Found {len(self.guides)} unique guides")
|
|
|
|
|
|
|
| 137 |
except Exception as e:
|
| 138 |
self.logger.error(f"Error in load_guides: {str(e)}")
|
| 139 |
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
|
|
| 150 |
|
| 151 |
return self._chromosome_seqs.get(chromosome)
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
def get_gene_data(self, locus_tag):
|
| 154 |
"""Get gene data with proper error handling"""
|
| 155 |
try:
|
|
|
|
| 161 |
if locus_tag in self._gene_data_cache:
|
| 162 |
return self._gene_data_cache[locus_tag]
|
| 163 |
|
| 164 |
+
# Ensure parser is initialized
|
| 165 |
+
if not self._ensure_annotation_parser():
|
| 166 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
# Get gene data from parser with proper string conversion
|
| 169 |
gene_data = None
|
| 170 |
if isinstance(locus_tag, (str, int)):
|
| 171 |
locus_tag_str = str(locus_tag).strip()
|
|
|
|
| 172 |
# Look up by locus tag directly
|
| 173 |
gene_data = self.annotation_parser.get_gene_data(locus_tag_str.lower())
|
| 174 |
|
| 175 |
if gene_data:
|
| 176 |
self._gene_data_cache[locus_tag] = gene_data
|
|
|
|
| 177 |
else:
|
| 178 |
self.logger.debug(f"No gene data found for locus tag: {locus_tag}")
|
| 179 |
|
|
|
|
| 207 |
self.logger.debug(f"View exons only is: {getattr(self, '_view_exons_only', False)}")
|
| 208 |
|
| 209 |
# Regular gene-based search
|
|
|
|
| 210 |
gene_data = self.get_gene_data(identifier)
|
| 211 |
if not gene_data or 'info' not in gene_data:
|
| 212 |
self.logger.warning(f"No gene data found for locus tag: {identifier}")
|
|
|
|
| 217 |
print(f"gene_data: {gene_data}")
|
| 218 |
full_location = gene_data['info'].get('full_location', '')
|
| 219 |
print(f"Full location: {full_location}")
|
| 220 |
+
if full_location:
|
| 221 |
+
if ',' in full_location: # Multiple exons
|
| 222 |
+
self.logger.debug(f"Processing exons from full location: {full_location}")
|
| 223 |
+
exon_sequences = []
|
| 224 |
+
full_sequence = gene_data['sequence'] # Use sequence from gene_data
|
| 225 |
+
|
| 226 |
+
# Calculate padding offset
|
| 227 |
+
padding = 30
|
| 228 |
+
gene_start = gene_data['info']['start']
|
| 229 |
+
padded_start = max(0, gene_start - padding)
|
| 230 |
+
padding_offset = gene_start - padded_start
|
| 231 |
+
|
| 232 |
+
print(f"gene_start: {gene_start}, padded_start: {padded_start}, padding_offset: {padding_offset}")
|
| 233 |
+
|
| 234 |
+
# Process each exon location
|
| 235 |
+
for exon in full_location.split(','):
|
| 236 |
+
# Extract coordinates and strand
|
| 237 |
+
coords = exon.split('(')[0] # Get part before strand
|
| 238 |
+
strand = exon.split('(')[1][0] # Get + or - from (+ or (-
|
| 239 |
+
start, end = map(int, coords.split('..'))
|
| 240 |
|
| 241 |
+
print(f"coords: {coords}, strand: {strand}, start: {start}, end: {end}")
|
| 242 |
+
|
| 243 |
+
# Adjust coordinates relative to gene start and account for padding
|
| 244 |
+
relative_start = start - gene_start + padding_offset
|
| 245 |
+
relative_end = end - gene_start + padding_offset
|
| 246 |
+
print(f"relative_start: {relative_start}, relative_end: {relative_end}")
|
| 247 |
+
|
| 248 |
+
# Get exon sequence from the padded sequence
|
| 249 |
+
exon_seq = full_sequence[relative_start:relative_end]
|
| 250 |
+
|
| 251 |
+
exon_sequences.append(exon_seq)
|
| 252 |
|
| 253 |
+
# Join exon sequences
|
| 254 |
+
sequence = ''.join(exon_sequences)
|
| 255 |
+
self.logger.debug(f"Created concatenated exon sequence of length: {len(sequence)}")
|
|
|
|
| 256 |
|
| 257 |
+
return {
|
| 258 |
+
'sequence': sequence,
|
| 259 |
+
'info': gene_data['info'],
|
| 260 |
+
'start': gene_data['info']['start'],
|
| 261 |
+
'end': gene_data['info']['end']
|
| 262 |
+
}
|
| 263 |
+
else: # Single location/exon
|
| 264 |
+
# Return sequence without padding for single exon
|
| 265 |
+
sequence = gene_data['sequence']
|
| 266 |
+
gene_start = gene_data['info']['start']
|
| 267 |
+
gene_end = gene_data['info']['end']
|
| 268 |
|
| 269 |
+
# Calculate padding offset
|
| 270 |
+
padding = 30
|
| 271 |
+
padded_start = max(0, gene_start - padding)
|
| 272 |
+
padding_offset = gene_start - padded_start
|
| 273 |
+
|
| 274 |
+
# Get sequence without padding
|
| 275 |
+
relative_start = padding_offset
|
| 276 |
+
relative_end = len(sequence) - padding_offset
|
| 277 |
+
sequence = sequence[relative_start:relative_end]
|
| 278 |
+
|
| 279 |
+
return {
|
| 280 |
+
'sequence': sequence,
|
| 281 |
+
'info': gene_data['info'],
|
| 282 |
+
'start': gene_data['info']['start'],
|
| 283 |
+
'end': gene_data['info']['end']
|
| 284 |
+
}
|
| 285 |
|
| 286 |
# If not in exons-only mode or no exons to process, return normal sequence
|
| 287 |
if 'sequence' in gene_data:
|
| 288 |
sequence = gene_data['sequence']
|
|
|
|
| 289 |
|
| 290 |
# Format sequence with padding in lowercase (only if not in exons-only mode)
|
| 291 |
if not hasattr(self, '_view_exons_only') or not self._view_exons_only:
|
|
|
|
| 312 |
'end': gene_data['info']['end'],
|
| 313 |
'full_location': gene_data['info'].get('full_location', '')
|
| 314 |
}
|
|
|
|
|
|
|
| 315 |
return result
|
| 316 |
|
| 317 |
self.logger.warning(f"No sequence data found in gene_data for {identifier}")
|
|
|
|
| 325 |
def _get_sequence_for_position(self, chrom, start, end):
|
| 326 |
"""Get sequence for a given position with proper padding handling"""
|
| 327 |
try:
|
| 328 |
+
if not self._ensure_annotation_parser():
|
| 329 |
+
return None
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
feature_info = {
|
| 332 |
'chromosome': chrom, # Use raw chromosome ID directly
|
| 333 |
+
'start': start, # Keep as is since annotation parser handles 0-based conversion
|
| 334 |
'end': end
|
| 335 |
}
|
| 336 |
|
| 337 |
self.logger.debug(f"Getting sequence for feature info: {feature_info}")
|
| 338 |
|
| 339 |
+
# Use annotation parser's method directly
|
| 340 |
+
sequence = self.annotation_parser._get_sequence_for_position(chrom, start, end)
|
| 341 |
if sequence:
|
| 342 |
padding = 30
|
| 343 |
|
| 344 |
# Handle start position padding
|
| 345 |
+
if start == 0: # Already 0-based for sequence operations
|
| 346 |
+
# No padding at start if starting at position 0
|
| 347 |
five_prime_pad = ""
|
| 348 |
main_sequence = sequence[:-(padding if len(sequence) > padding else 0)].upper()
|
| 349 |
else:
|
|
@@ -55,31 +55,6 @@
|
|
| 55 |
</item>
|
| 56 |
<item row="3" column="0" rowspan="2" colspan="2">
|
| 57 |
<layout class="QHBoxLayout" name="horizontalLayout">
|
| 58 |
-
<item alignment="Qt::AlignLeft">
|
| 59 |
-
<widget class="QPushButton" name="pbtnBack">
|
| 60 |
-
<property name="sizePolicy">
|
| 61 |
-
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
| 62 |
-
<horstretch>0</horstretch>
|
| 63 |
-
<verstretch>0</verstretch>
|
| 64 |
-
</sizepolicy>
|
| 65 |
-
</property>
|
| 66 |
-
<property name="minimumSize">
|
| 67 |
-
<size>
|
| 68 |
-
<width>125</width>
|
| 69 |
-
<height>0</height>
|
| 70 |
-
</size>
|
| 71 |
-
</property>
|
| 72 |
-
<property name="maximumSize">
|
| 73 |
-
<size>
|
| 74 |
-
<width>16777215</width>
|
| 75 |
-
<height>16777215</height>
|
| 76 |
-
</size>
|
| 77 |
-
</property>
|
| 78 |
-
<property name="text">
|
| 79 |
-
<string>Return</string>
|
| 80 |
-
</property>
|
| 81 |
-
</widget>
|
| 82 |
-
</item>
|
| 83 |
<item>
|
| 84 |
<widget class="QPushButton" name="pbtnGenerateLibrary">
|
| 85 |
<property name="sizePolicy">
|
|
|
|
| 55 |
</item>
|
| 56 |
<item row="3" column="0" rowspan="2" colspan="2">
|
| 57 |
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
<item>
|
| 59 |
<widget class="QPushButton" name="pbtnGenerateLibrary">
|
| 60 |
<property name="sizePolicy">
|
|
@@ -10,7 +10,7 @@
|
|
| 10 |
<x>0</x>
|
| 11 |
<y>0</y>
|
| 12 |
<width>960</width>
|
| 13 |
-
<height>
|
| 14 |
</rect>
|
| 15 |
</property>
|
| 16 |
<property name="font">
|
|
@@ -50,7 +50,50 @@
|
|
| 50 |
<string notr="true"/>
|
| 51 |
</property>
|
| 52 |
<layout class="QGridLayout" name="gridContainer">
|
| 53 |
-
<item row="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
<layout class="QGridLayout" name="gridStep1Step2Step3">
|
| 55 |
<property name="sizeConstraint">
|
| 56 |
<enum>QLayout::SetDefaultConstraint</enum>
|
|
@@ -551,112 +594,6 @@
|
|
| 551 |
</item>
|
| 552 |
</layout>
|
| 553 |
</item>
|
| 554 |
-
<item row="0" column="0">
|
| 555 |
-
<widget class="QLabel" name="lblWindowHeading">
|
| 556 |
-
<property name="sizePolicy">
|
| 557 |
-
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
| 558 |
-
<horstretch>0</horstretch>
|
| 559 |
-
<verstretch>0</verstretch>
|
| 560 |
-
</sizepolicy>
|
| 561 |
-
</property>
|
| 562 |
-
<property name="minimumSize">
|
| 563 |
-
<size>
|
| 564 |
-
<width>0</width>
|
| 565 |
-
<height>0</height>
|
| 566 |
-
</size>
|
| 567 |
-
</property>
|
| 568 |
-
<property name="maximumSize">
|
| 569 |
-
<size>
|
| 570 |
-
<width>16777215</width>
|
| 571 |
-
<height>16777215</height>
|
| 572 |
-
</size>
|
| 573 |
-
</property>
|
| 574 |
-
<property name="font">
|
| 575 |
-
<font>
|
| 576 |
-
<family>Arial</family>
|
| 577 |
-
<pointsize>12</pointsize>
|
| 578 |
-
<weight>75</weight>
|
| 579 |
-
<italic>false</italic>
|
| 580 |
-
<bold>true</bold>
|
| 581 |
-
</font>
|
| 582 |
-
</property>
|
| 583 |
-
<property name="styleSheet">
|
| 584 |
-
<string notr="true"/>
|
| 585 |
-
</property>
|
| 586 |
-
<property name="text">
|
| 587 |
-
<string>CASPER</string>
|
| 588 |
-
</property>
|
| 589 |
-
</widget>
|
| 590 |
-
</item>
|
| 591 |
-
<item row="0" column="1">
|
| 592 |
-
<widget class="QLabel" name="pbtnThemeToggle">
|
| 593 |
-
<property name="minimumSize">
|
| 594 |
-
<size>
|
| 595 |
-
<width>50</width>
|
| 596 |
-
<height>0</height>
|
| 597 |
-
</size>
|
| 598 |
-
</property>
|
| 599 |
-
<property name="maximumSize">
|
| 600 |
-
<size>
|
| 601 |
-
<width>50</width>
|
| 602 |
-
<height>16777215</height>
|
| 603 |
-
</size>
|
| 604 |
-
</property>
|
| 605 |
-
<property name="text">
|
| 606 |
-
<string/>
|
| 607 |
-
</property>
|
| 608 |
-
</widget>
|
| 609 |
-
</item>
|
| 610 |
-
<item row="2" column="0" colspan="2">
|
| 611 |
-
<widget class="QGroupBox" name="grpNavigationMenu">
|
| 612 |
-
<property name="sizePolicy">
|
| 613 |
-
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
| 614 |
-
<horstretch>0</horstretch>
|
| 615 |
-
<verstretch>0</verstretch>
|
| 616 |
-
</sizepolicy>
|
| 617 |
-
</property>
|
| 618 |
-
<property name="title">
|
| 619 |
-
<string>CASPER Navigation</string>
|
| 620 |
-
</property>
|
| 621 |
-
<layout class="QGridLayout" name="gridLayout_2">
|
| 622 |
-
<item row="1" column="1">
|
| 623 |
-
<widget class="QPushButton" name="pbtnPopulationAnalysis">
|
| 624 |
-
<property name="text">
|
| 625 |
-
<string>Population Analysis</string>
|
| 626 |
-
</property>
|
| 627 |
-
</widget>
|
| 628 |
-
</item>
|
| 629 |
-
<item row="0" column="1">
|
| 630 |
-
<widget class="QPushButton" name="pbtnNewEndonuclease">
|
| 631 |
-
<property name="text">
|
| 632 |
-
<string>Define New Endonuclease</string>
|
| 633 |
-
</property>
|
| 634 |
-
</widget>
|
| 635 |
-
</item>
|
| 636 |
-
<item row="1" column="0">
|
| 637 |
-
<widget class="QPushButton" name="pbtnMultitargetingAnalysis">
|
| 638 |
-
<property name="text">
|
| 639 |
-
<string>Multitargeting Analysis</string>
|
| 640 |
-
</property>
|
| 641 |
-
</widget>
|
| 642 |
-
</item>
|
| 643 |
-
<item row="2" column="0">
|
| 644 |
-
<widget class="QPushButton" name="pbtnCombineFiles">
|
| 645 |
-
<property name="text">
|
| 646 |
-
<string>Combine Files</string>
|
| 647 |
-
</property>
|
| 648 |
-
</widget>
|
| 649 |
-
</item>
|
| 650 |
-
<item row="0" column="0">
|
| 651 |
-
<widget class="QPushButton" name="pbtnNewGenome">
|
| 652 |
-
<property name="text">
|
| 653 |
-
<string>Analyze New Genome</string>
|
| 654 |
-
</property>
|
| 655 |
-
</widget>
|
| 656 |
-
</item>
|
| 657 |
-
</layout>
|
| 658 |
-
</widget>
|
| 659 |
-
</item>
|
| 660 |
</layout>
|
| 661 |
</widget>
|
| 662 |
<action name="actionNew">
|
|
|
|
| 10 |
<x>0</x>
|
| 11 |
<y>0</y>
|
| 12 |
<width>960</width>
|
| 13 |
+
<height>916</height>
|
| 14 |
</rect>
|
| 15 |
</property>
|
| 16 |
<property name="font">
|
|
|
|
| 50 |
<string notr="true"/>
|
| 51 |
</property>
|
| 52 |
<layout class="QGridLayout" name="gridContainer">
|
| 53 |
+
<item row="1" column="0" colspan="2">
|
| 54 |
+
<widget class="QGroupBox" name="grpNavigationMenu">
|
| 55 |
+
<property name="sizePolicy">
|
| 56 |
+
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
| 57 |
+
<horstretch>0</horstretch>
|
| 58 |
+
<verstretch>0</verstretch>
|
| 59 |
+
</sizepolicy>
|
| 60 |
+
</property>
|
| 61 |
+
<property name="title">
|
| 62 |
+
<string>CASPER Navigation</string>
|
| 63 |
+
</property>
|
| 64 |
+
<layout class="QGridLayout" name="gridLayout_2">
|
| 65 |
+
<item row="1" column="1">
|
| 66 |
+
<widget class="QPushButton" name="pbtnPopulationAnalysis">
|
| 67 |
+
<property name="text">
|
| 68 |
+
<string>Population Analysis</string>
|
| 69 |
+
</property>
|
| 70 |
+
</widget>
|
| 71 |
+
</item>
|
| 72 |
+
<item row="0" column="1">
|
| 73 |
+
<widget class="QPushButton" name="pbtnNewEndonuclease">
|
| 74 |
+
<property name="text">
|
| 75 |
+
<string>Define New Endonuclease</string>
|
| 76 |
+
</property>
|
| 77 |
+
</widget>
|
| 78 |
+
</item>
|
| 79 |
+
<item row="0" column="0">
|
| 80 |
+
<widget class="QPushButton" name="pbtnNewGenome">
|
| 81 |
+
<property name="text">
|
| 82 |
+
<string>Analyze New Genome</string>
|
| 83 |
+
</property>
|
| 84 |
+
</widget>
|
| 85 |
+
</item>
|
| 86 |
+
<item row="1" column="0">
|
| 87 |
+
<widget class="QPushButton" name="pbtnMultitargetingAnalysis">
|
| 88 |
+
<property name="text">
|
| 89 |
+
<string>Multitargeting Analysis</string>
|
| 90 |
+
</property>
|
| 91 |
+
</widget>
|
| 92 |
+
</item>
|
| 93 |
+
</layout>
|
| 94 |
+
</widget>
|
| 95 |
+
</item>
|
| 96 |
+
<item row="3" column="0" rowspan="2" colspan="2">
|
| 97 |
<layout class="QGridLayout" name="gridStep1Step2Step3">
|
| 98 |
<property name="sizeConstraint">
|
| 99 |
<enum>QLayout::SetDefaultConstraint</enum>
|
|
|
|
| 594 |
</item>
|
| 595 |
</layout>
|
| 596 |
</item>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
</layout>
|
| 598 |
</widget>
|
| 599 |
<action name="actionNew">
|
|
@@ -71,7 +71,7 @@
|
|
| 71 |
<x>0</x>
|
| 72 |
<y>0</y>
|
| 73 |
<width>901</width>
|
| 74 |
-
<height>
|
| 75 |
</rect>
|
| 76 |
</property>
|
| 77 |
<property name="font">
|
|
@@ -83,28 +83,6 @@
|
|
| 83 |
<bold>false</bold>
|
| 84 |
</font>
|
| 85 |
</property>
|
| 86 |
-
<widget class="QMenu" name="menuFile">
|
| 87 |
-
<property name="title">
|
| 88 |
-
<string>File</string>
|
| 89 |
-
</property>
|
| 90 |
-
<addaction name="actionExit"/>
|
| 91 |
-
<addaction name="actChangeDatabaseDirectory"/>
|
| 92 |
-
</widget>
|
| 93 |
-
<widget class="QMenu" name="menuTools">
|
| 94 |
-
<property name="title">
|
| 95 |
-
<string>Tools</string>
|
| 96 |
-
</property>
|
| 97 |
-
<addaction name="actOpenGenomeBrowser"/>
|
| 98 |
-
</widget>
|
| 99 |
-
<widget class="QMenu" name="menuLinks">
|
| 100 |
-
<property name="title">
|
| 101 |
-
<string>Links</string>
|
| 102 |
-
</property>
|
| 103 |
-
<addaction name="actGoToNCBI"/>
|
| 104 |
-
<addaction name="actionGoToNCBIBLAST"/>
|
| 105 |
-
<addaction name="separator"/>
|
| 106 |
-
<addaction name="actionGoToCASPERRepository"/>
|
| 107 |
-
</widget>
|
| 108 |
<widget class="QMenu" name="menuCASPER">
|
| 109 |
<property name="title">
|
| 110 |
<string>CASPER</string>
|
|
@@ -112,9 +90,6 @@
|
|
| 112 |
<addaction name="actionAbout_CASPER"/>
|
| 113 |
</widget>
|
| 114 |
<addaction name="menuCASPER"/>
|
| 115 |
-
<addaction name="menuFile"/>
|
| 116 |
-
<addaction name="menuTools"/>
|
| 117 |
-
<addaction name="menuLinks"/>
|
| 118 |
</widget>
|
| 119 |
<widget class="QToolBar" name="toolBar">
|
| 120 |
<property name="windowTitle">
|
|
|
|
| 71 |
<x>0</x>
|
| 72 |
<y>0</y>
|
| 73 |
<width>901</width>
|
| 74 |
+
<height>24</height>
|
| 75 |
</rect>
|
| 76 |
</property>
|
| 77 |
<property name="font">
|
|
|
|
| 83 |
<bold>false</bold>
|
| 84 |
</font>
|
| 85 |
</property>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
<widget class="QMenu" name="menuCASPER">
|
| 87 |
<property name="title">
|
| 88 |
<string>CASPER</string>
|
|
|
|
| 90 |
<addaction name="actionAbout_CASPER"/>
|
| 91 |
</widget>
|
| 92 |
<addaction name="menuCASPER"/>
|
|
|
|
|
|
|
|
|
|
| 93 |
</widget>
|
| 94 |
<widget class="QToolBar" name="toolBar">
|
| 95 |
<property name="windowTitle">
|
|
@@ -502,40 +502,6 @@ search parameters</string>
|
|
| 502 |
</item>
|
| 503 |
</layout>
|
| 504 |
</item>
|
| 505 |
-
<item row="0" column="1">
|
| 506 |
-
<widget class="QLabel" name="lblWindowHeading">
|
| 507 |
-
<property name="minimumSize">
|
| 508 |
-
<size>
|
| 509 |
-
<width>0</width>
|
| 510 |
-
<height>0</height>
|
| 511 |
-
</size>
|
| 512 |
-
</property>
|
| 513 |
-
<property name="maximumSize">
|
| 514 |
-
<size>
|
| 515 |
-
<width>16777215</width>
|
| 516 |
-
<height>50</height>
|
| 517 |
-
</size>
|
| 518 |
-
</property>
|
| 519 |
-
<property name="font">
|
| 520 |
-
<font>
|
| 521 |
-
<family>Arial</family>
|
| 522 |
-
<pointsize>12</pointsize>
|
| 523 |
-
<weight>75</weight>
|
| 524 |
-
<italic>false</italic>
|
| 525 |
-
<bold>true</bold>
|
| 526 |
-
</font>
|
| 527 |
-
</property>
|
| 528 |
-
<property name="styleSheet">
|
| 529 |
-
<string notr="true"/>
|
| 530 |
-
</property>
|
| 531 |
-
<property name="text">
|
| 532 |
-
<string>Analyze New Genome</string>
|
| 533 |
-
</property>
|
| 534 |
-
<property name="scaledContents">
|
| 535 |
-
<bool>false</bool>
|
| 536 |
-
</property>
|
| 537 |
-
</widget>
|
| 538 |
-
</item>
|
| 539 |
<item row="3" column="1" colspan="2">
|
| 540 |
<widget class="QGroupBox" name="Step3">
|
| 541 |
<property name="sizePolicy">
|
|
|
|
| 502 |
</item>
|
| 503 |
</layout>
|
| 504 |
</item>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
<item row="3" column="1" colspan="2">
|
| 506 |
<widget class="QGroupBox" name="Step3">
|
| 507 |
<property name="sizePolicy">
|
|
@@ -6,8 +6,8 @@
|
|
| 6 |
<rect>
|
| 7 |
<x>0</x>
|
| 8 |
<y>0</y>
|
| 9 |
-
<width>
|
| 10 |
-
<height>
|
| 11 |
</rect>
|
| 12 |
</property>
|
| 13 |
<property name="font">
|
|
@@ -22,68 +22,6 @@
|
|
| 22 |
<layout class="QGridLayout" name="gridLayout_2">
|
| 23 |
<item row="0" column="0">
|
| 24 |
<layout class="QGridLayout" name="gridLayout">
|
| 25 |
-
<item row="0" column="1" rowspan="2">
|
| 26 |
-
<widget class="QGroupBox" name="grpGeneViewer">
|
| 27 |
-
<property name="sizePolicy">
|
| 28 |
-
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
| 29 |
-
<horstretch>0</horstretch>
|
| 30 |
-
<verstretch>0</verstretch>
|
| 31 |
-
</sizepolicy>
|
| 32 |
-
</property>
|
| 33 |
-
<property name="title">
|
| 34 |
-
<string>Gene Viewer</string>
|
| 35 |
-
</property>
|
| 36 |
-
<layout class="QGridLayout" name="gridLayout_6">
|
| 37 |
-
<item row="2" column="1">
|
| 38 |
-
<widget class="QLineEdit" name="ledStartLocation"/>
|
| 39 |
-
</item>
|
| 40 |
-
<item row="3" column="2">
|
| 41 |
-
<widget class="QPushButton" name="pbtnResetLocation">
|
| 42 |
-
<property name="text">
|
| 43 |
-
<string>Reset Location</string>
|
| 44 |
-
</property>
|
| 45 |
-
</widget>
|
| 46 |
-
</item>
|
| 47 |
-
<item row="2" column="0">
|
| 48 |
-
<widget class="QLabel" name="lblStartLocation">
|
| 49 |
-
<property name="text">
|
| 50 |
-
<string>Start:</string>
|
| 51 |
-
</property>
|
| 52 |
-
</widget>
|
| 53 |
-
</item>
|
| 54 |
-
<item row="3" column="1">
|
| 55 |
-
<widget class="QLineEdit" name="ledStopLocation"/>
|
| 56 |
-
</item>
|
| 57 |
-
<item row="3" column="0">
|
| 58 |
-
<widget class="QLabel" name="lblStopLocation">
|
| 59 |
-
<property name="text">
|
| 60 |
-
<string>Stop:</string>
|
| 61 |
-
</property>
|
| 62 |
-
</widget>
|
| 63 |
-
</item>
|
| 64 |
-
<item row="2" column="2">
|
| 65 |
-
<widget class="QPushButton" name="pbtnChangeLocation">
|
| 66 |
-
<property name="toolTip">
|
| 67 |
-
<string><html><head/><body><p>This button changes the start and end location of the Gene Viewer sequence, based on what is entered in the Start and Stop boxes.</p></body></html></string>
|
| 68 |
-
</property>
|
| 69 |
-
<property name="text">
|
| 70 |
-
<string>Change Location</string>
|
| 71 |
-
</property>
|
| 72 |
-
</widget>
|
| 73 |
-
</item>
|
| 74 |
-
<item row="5" column="0" colspan="5">
|
| 75 |
-
<widget class="QTextEdit" name="txtedGeneViewer"/>
|
| 76 |
-
</item>
|
| 77 |
-
<item row="4" column="2">
|
| 78 |
-
<widget class="QCheckBox" name="chkViewExonsOnly">
|
| 79 |
-
<property name="text">
|
| 80 |
-
<string>View Exons Only</string>
|
| 81 |
-
</property>
|
| 82 |
-
</widget>
|
| 83 |
-
</item>
|
| 84 |
-
</layout>
|
| 85 |
-
</widget>
|
| 86 |
-
</item>
|
| 87 |
<item row="0" column="0">
|
| 88 |
<widget class="QGroupBox" name="grpGuideViewer">
|
| 89 |
<property name="sizePolicy">
|
|
@@ -217,6 +155,93 @@
|
|
| 217 |
</layout>
|
| 218 |
</widget>
|
| 219 |
</item>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
</layout>
|
| 221 |
</item>
|
| 222 |
</layout>
|
|
|
|
| 6 |
<rect>
|
| 7 |
<x>0</x>
|
| 8 |
<y>0</y>
|
| 9 |
+
<width>1512</width>
|
| 10 |
+
<height>916</height>
|
| 11 |
</rect>
|
| 12 |
</property>
|
| 13 |
<property name="font">
|
|
|
|
| 22 |
<layout class="QGridLayout" name="gridLayout_2">
|
| 23 |
<item row="0" column="0">
|
| 24 |
<layout class="QGridLayout" name="gridLayout">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
<item row="0" column="0">
|
| 26 |
<widget class="QGroupBox" name="grpGuideViewer">
|
| 27 |
<property name="sizePolicy">
|
|
|
|
| 155 |
</layout>
|
| 156 |
</widget>
|
| 157 |
</item>
|
| 158 |
+
<item row="0" column="1">
|
| 159 |
+
<widget class="QGroupBox" name="grpGeneViewer">
|
| 160 |
+
<property name="sizePolicy">
|
| 161 |
+
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
| 162 |
+
<horstretch>0</horstretch>
|
| 163 |
+
<verstretch>0</verstretch>
|
| 164 |
+
</sizepolicy>
|
| 165 |
+
</property>
|
| 166 |
+
<property name="title">
|
| 167 |
+
<string>Gene Viewer</string>
|
| 168 |
+
</property>
|
| 169 |
+
<layout class="QGridLayout" name="gridLayout_6">
|
| 170 |
+
<item row="3" column="1">
|
| 171 |
+
<widget class="QLineEdit" name="ledStopLocation"/>
|
| 172 |
+
</item>
|
| 173 |
+
<item row="4" column="0" colspan="2">
|
| 174 |
+
<widget class="QLabel" name="lblSequenceLegend">
|
| 175 |
+
<property name="sizePolicy">
|
| 176 |
+
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
| 177 |
+
<horstretch>0</horstretch>
|
| 178 |
+
<verstretch>0</verstretch>
|
| 179 |
+
</sizepolicy>
|
| 180 |
+
</property>
|
| 181 |
+
<property name="layoutDirection">
|
| 182 |
+
<enum>Qt::LeftToRight</enum>
|
| 183 |
+
</property>
|
| 184 |
+
<property name="text">
|
| 185 |
+
<string>ACTG - Gene Sequence&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color: rgb(100, 100, 100);">actg</span> - Padding Sequence</string>
|
| 186 |
+
</property>
|
| 187 |
+
</widget>
|
| 188 |
+
</item>
|
| 189 |
+
<item row="2" column="1">
|
| 190 |
+
<widget class="QLineEdit" name="ledStartLocation"/>
|
| 191 |
+
</item>
|
| 192 |
+
<item row="3" column="2">
|
| 193 |
+
<widget class="QPushButton" name="pbtnResetLocation">
|
| 194 |
+
<property name="text">
|
| 195 |
+
<string>Reset Location</string>
|
| 196 |
+
</property>
|
| 197 |
+
</widget>
|
| 198 |
+
</item>
|
| 199 |
+
<item row="3" column="0">
|
| 200 |
+
<widget class="QLabel" name="lblStopLocation">
|
| 201 |
+
<property name="text">
|
| 202 |
+
<string>Stop:</string>
|
| 203 |
+
</property>
|
| 204 |
+
</widget>
|
| 205 |
+
</item>
|
| 206 |
+
<item row="4" column="2">
|
| 207 |
+
<widget class="QCheckBox" name="chkViewExonsOnly">
|
| 208 |
+
<property name="autoFillBackground">
|
| 209 |
+
<bool>false</bool>
|
| 210 |
+
</property>
|
| 211 |
+
<property name="text">
|
| 212 |
+
<string>View Exons Only</string>
|
| 213 |
+
</property>
|
| 214 |
+
</widget>
|
| 215 |
+
</item>
|
| 216 |
+
<item row="5" column="0" colspan="5">
|
| 217 |
+
<widget class="QTextEdit" name="txtedGeneViewer"/>
|
| 218 |
+
</item>
|
| 219 |
+
<item row="2" column="0">
|
| 220 |
+
<widget class="QLabel" name="lblStartLocation">
|
| 221 |
+
<property name="text">
|
| 222 |
+
<string>Start:</string>
|
| 223 |
+
</property>
|
| 224 |
+
</widget>
|
| 225 |
+
</item>
|
| 226 |
+
<item row="2" column="2">
|
| 227 |
+
<widget class="QPushButton" name="pbtnChangeLocation">
|
| 228 |
+
<property name="sizePolicy">
|
| 229 |
+
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
| 230 |
+
<horstretch>0</horstretch>
|
| 231 |
+
<verstretch>0</verstretch>
|
| 232 |
+
</sizepolicy>
|
| 233 |
+
</property>
|
| 234 |
+
<property name="toolTip">
|
| 235 |
+
<string><html><head/><body><p>This button changes the start and end location of the Gene Viewer sequence, based on what is entered in the Start and Stop boxes.</p></body></html></string>
|
| 236 |
+
</property>
|
| 237 |
+
<property name="text">
|
| 238 |
+
<string>Change Location</string>
|
| 239 |
+
</property>
|
| 240 |
+
</widget>
|
| 241 |
+
</item>
|
| 242 |
+
</layout>
|
| 243 |
+
</widget>
|
| 244 |
+
</item>
|
| 245 |
</layout>
|
| 246 |
</item>
|
| 247 |
</layout>
|
|
@@ -6,6 +6,7 @@ import logging
|
|
| 6 |
|
| 7 |
class CloseableTabWidget(QTabWidget):
|
| 8 |
tab_closed = pyqtSignal(QWidget)
|
|
|
|
| 9 |
|
| 10 |
def __init__(self, parent=None):
|
| 11 |
super().__init__(parent)
|
|
@@ -19,27 +20,32 @@ class CloseableTabWidget(QTabWidget):
|
|
| 19 |
"""Close a tab at the given index"""
|
| 20 |
self.logger.debug(f"Attempting to close tab at index {index}")
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
widget = self.widget(index)
|
| 27 |
if not widget:
|
| 28 |
self.logger.warning(f"No widget found at index {index}")
|
| 29 |
return
|
| 30 |
-
|
| 31 |
-
# Critical operations need try-catch
|
| 32 |
try:
|
|
|
|
|
|
|
|
|
|
| 33 |
tab_text = self.tabText(index)
|
| 34 |
|
| 35 |
# Cleanup controller if exists
|
| 36 |
controller = getattr(widget, 'controller', None)
|
| 37 |
-
if controller and hasattr(controller, 'model') and hasattr(controller.model, 'cleanup'):
|
| 38 |
-
controller.model.cleanup()
|
| 39 |
|
| 40 |
# Remove from tracking and emit signal
|
| 41 |
-
|
| 42 |
-
|
|
|
|
| 43 |
|
| 44 |
self.removeTab(index)
|
| 45 |
self.tab_closed.emit(widget)
|
|
@@ -116,6 +122,8 @@ class CloseableTabWidget(QTabWidget):
|
|
| 116 |
if 0 <= index < self.count():
|
| 117 |
current_widget = self.widget(index)
|
| 118 |
if current_widget and index != 0:
|
|
|
|
|
|
|
| 119 |
self.closeTab(index)
|
| 120 |
except Exception as e:
|
| 121 |
self.logger.error(f"Error in safely_close_tab: {e}")
|
|
@@ -166,4 +174,28 @@ class CloseableTabWidget(QTabWidget):
|
|
| 166 |
self._update_all_tabs()
|
| 167 |
|
| 168 |
except Exception as e:
|
| 169 |
-
self.logger.error(f"Error moving tab: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
class CloseableTabWidget(QTabWidget):
|
| 8 |
tab_closed = pyqtSignal(QWidget)
|
| 9 |
+
tab_closing = pyqtSignal(int)
|
| 10 |
|
| 11 |
def __init__(self, parent=None):
|
| 12 |
super().__init__(parent)
|
|
|
|
| 20 |
"""Close a tab at the given index"""
|
| 21 |
self.logger.debug(f"Attempting to close tab at index {index}")
|
| 22 |
|
| 23 |
+
# Skip normal closure conditions for forced closure
|
| 24 |
+
if not hasattr(self, '_force_close'):
|
| 25 |
+
if not (self.count() > 1 and index != 0):
|
| 26 |
+
self.logger.debug("Tab closure conditions not met")
|
| 27 |
+
return
|
| 28 |
+
|
| 29 |
widget = self.widget(index)
|
| 30 |
if not widget:
|
| 31 |
self.logger.warning(f"No widget found at index {index}")
|
| 32 |
return
|
| 33 |
+
|
|
|
|
| 34 |
try:
|
| 35 |
+
# Emit signal before closing the tab
|
| 36 |
+
self.tab_closing.emit(index)
|
| 37 |
+
|
| 38 |
tab_text = self.tabText(index)
|
| 39 |
|
| 40 |
# Cleanup controller if exists
|
| 41 |
controller = getattr(widget, 'controller', None)
|
| 42 |
+
# if controller and hasattr(controller, 'model') and hasattr(controller.model, 'cleanup'):
|
| 43 |
+
# controller.model.cleanup()
|
| 44 |
|
| 45 |
# Remove from tracking and emit signal
|
| 46 |
+
tab_id = f"{tab_text}_{id(widget)}"
|
| 47 |
+
if tab_id in self._tabs:
|
| 48 |
+
del self._tabs[tab_id]
|
| 49 |
|
| 50 |
self.removeTab(index)
|
| 51 |
self.tab_closed.emit(widget)
|
|
|
|
| 122 |
if 0 <= index < self.count():
|
| 123 |
current_widget = self.widget(index)
|
| 124 |
if current_widget and index != 0:
|
| 125 |
+
# Emit signal before closing
|
| 126 |
+
self.tab_closing.emit(index)
|
| 127 |
self.closeTab(index)
|
| 128 |
except Exception as e:
|
| 129 |
self.logger.error(f"Error in safely_close_tab: {e}")
|
|
|
|
| 174 |
self._update_all_tabs()
|
| 175 |
|
| 176 |
except Exception as e:
|
| 177 |
+
self.logger.error(f"Error moving tab: {e}")
|
| 178 |
+
|
| 179 |
+
def removeTab(self, index):
|
| 180 |
+
"""Override removeTab to handle cleanup"""
|
| 181 |
+
try:
|
| 182 |
+
widget = self.widget(index)
|
| 183 |
+
if widget:
|
| 184 |
+
# Get tab text before removal
|
| 185 |
+
tab_text = self.tabText(index)
|
| 186 |
+
|
| 187 |
+
# Remove from tracking dictionary
|
| 188 |
+
tab_id = f"{tab_text}_{id(widget)}"
|
| 189 |
+
if tab_id in self._tabs:
|
| 190 |
+
del self._tabs[tab_id]
|
| 191 |
+
|
| 192 |
+
# Remove the tab
|
| 193 |
+
super().removeTab(index)
|
| 194 |
+
|
| 195 |
+
# Cleanup the widget
|
| 196 |
+
widget.deleteLater()
|
| 197 |
+
|
| 198 |
+
self._update_all_tabs()
|
| 199 |
+
|
| 200 |
+
except Exception as e:
|
| 201 |
+
self.logger.error(f"Error removing tab: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from PyQt6 import QtWidgets
|
| 2 |
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
| 3 |
-
QPushButton, QHBoxLayout, QLabel, QAbstractItemView)
|
| 4 |
from PyQt6 import uic
|
| 5 |
from PyQt6.QtCore import Qt, QTimer
|
| 6 |
import time
|
|
@@ -17,10 +17,15 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 17 |
|
| 18 |
def _init_ui(self):
|
| 19 |
uic.loadUi(self.global_settings.get_ui_dir_path() + '/find_targets.ui', self)
|
|
|
|
| 20 |
self.results_table = self.findChild(QTableWidget, 'tblTargets')
|
| 21 |
|
|
|
|
|
|
|
|
|
|
| 22 |
# Optimize table settings for large datasets
|
| 23 |
self.results_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
|
|
| 24 |
self.results_table.setShowGrid(False)
|
| 25 |
self.results_table.setAlternatingRowColors(True)
|
| 26 |
|
|
@@ -134,23 +139,28 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 134 |
selected_rows = set(index.row() for index in self.results_table.selectedIndexes())
|
| 135 |
selected_targets = []
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
# Get the currently visible rows from the table
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
# Get data from visible row
|
| 142 |
target_data = {
|
| 143 |
-
'feature_type':
|
| 144 |
-
'chromosome':
|
| 145 |
-
'feature_id':
|
| 146 |
-
'feature_name':
|
| 147 |
-
'feature_description':
|
| 148 |
}
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
# Match selected rows with visible targets
|
| 152 |
-
for row, target_data in visible_targets:
|
| 153 |
-
if row in selected_rows:
|
| 154 |
# Find corresponding full target data from _all_results
|
| 155 |
for full_target in self._all_results:
|
| 156 |
if (full_target['feature_id'] == target_data['feature_id'] and
|
|
@@ -158,6 +168,10 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 158 |
selected_targets.append(full_target)
|
| 159 |
break
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
self.logger.debug(f"Selected {len(selected_targets)} targets from filtered view")
|
| 162 |
return selected_targets
|
| 163 |
|
|
@@ -173,8 +187,7 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 173 |
"""Handle generate library button click"""
|
| 174 |
try:
|
| 175 |
selected_targets = self.get_selected_targets()
|
| 176 |
-
|
| 177 |
-
|
| 178 |
if not selected_targets:
|
| 179 |
QtWidgets.QMessageBox.warning(
|
| 180 |
self,
|
|
@@ -183,14 +196,15 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 183 |
)
|
| 184 |
return
|
| 185 |
|
|
|
|
|
|
|
|
|
|
| 186 |
# Create and show generate library window
|
| 187 |
-
self.global_settings.logger.debug("Creating GenerateLibraryController")
|
| 188 |
from controllers.GenerateLibraryController import GenerateLibraryController
|
| 189 |
generate_library_controller = GenerateLibraryController(
|
| 190 |
self.global_settings,
|
| 191 |
selected_targets
|
| 192 |
)
|
| 193 |
-
self.global_settings.logger.debug("Showing generate library window")
|
| 194 |
generate_library_controller.show()
|
| 195 |
|
| 196 |
except Exception as e:
|
|
@@ -200,3 +214,15 @@ class FindTargetsView(QtWidgets.QMainWindow):
|
|
| 200 |
"Error",
|
| 201 |
f"An error occurred while opening the generate library window: {str(e)}"
|
| 202 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from PyQt6 import QtWidgets
|
| 2 |
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
| 3 |
+
QPushButton, QHBoxLayout, QLabel, QAbstractItemView, QCheckBox)
|
| 4 |
from PyQt6 import uic
|
| 5 |
from PyQt6.QtCore import Qt, QTimer
|
| 6 |
import time
|
|
|
|
| 17 |
|
| 18 |
def _init_ui(self):
|
| 19 |
uic.loadUi(self.global_settings.get_ui_dir_path() + '/find_targets.ui', self)
|
| 20 |
+
self.checkbox_select_all = self.findChild(QCheckBox, 'chkSelectAll')
|
| 21 |
self.results_table = self.findChild(QTableWidget, 'tblTargets')
|
| 22 |
|
| 23 |
+
# Connect select all checkbox signal
|
| 24 |
+
self.checkbox_select_all.stateChanged.connect(self._on_select_all_changed)
|
| 25 |
+
|
| 26 |
# Optimize table settings for large datasets
|
| 27 |
self.results_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
| 28 |
+
self.results_table.setSelectionMode(QTableWidget.SelectionMode.MultiSelection)
|
| 29 |
self.results_table.setShowGrid(False)
|
| 30 |
self.results_table.setAlternatingRowColors(True)
|
| 31 |
|
|
|
|
| 139 |
selected_rows = set(index.row() for index in self.results_table.selectedIndexes())
|
| 140 |
selected_targets = []
|
| 141 |
|
| 142 |
+
if not selected_rows:
|
| 143 |
+
self.logger.debug("No rows selected")
|
| 144 |
+
return []
|
| 145 |
+
|
| 146 |
# Get the currently visible rows from the table
|
| 147 |
+
for row in selected_rows:
|
| 148 |
+
try:
|
| 149 |
+
# Check if all required cells have valid data
|
| 150 |
+
cells = [self.results_table.item(row, col) for col in range(5)]
|
| 151 |
+
if any(cell is None for cell in cells):
|
| 152 |
+
self.logger.warning(f"Row {row} has missing data, skipping")
|
| 153 |
+
continue
|
| 154 |
+
|
| 155 |
# Get data from visible row
|
| 156 |
target_data = {
|
| 157 |
+
'feature_type': cells[0].text(),
|
| 158 |
+
'chromosome': cells[1].text(),
|
| 159 |
+
'feature_id': cells[2].text(),
|
| 160 |
+
'feature_name': cells[3].text(),
|
| 161 |
+
'feature_description': cells[4].text()
|
| 162 |
}
|
| 163 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
# Find corresponding full target data from _all_results
|
| 165 |
for full_target in self._all_results:
|
| 166 |
if (full_target['feature_id'] == target_data['feature_id'] and
|
|
|
|
| 168 |
selected_targets.append(full_target)
|
| 169 |
break
|
| 170 |
|
| 171 |
+
except Exception as row_error:
|
| 172 |
+
self.logger.warning(f"Error processing row {row}: {str(row_error)}")
|
| 173 |
+
continue
|
| 174 |
+
|
| 175 |
self.logger.debug(f"Selected {len(selected_targets)} targets from filtered view")
|
| 176 |
return selected_targets
|
| 177 |
|
|
|
|
| 187 |
"""Handle generate library button click"""
|
| 188 |
try:
|
| 189 |
selected_targets = self.get_selected_targets()
|
| 190 |
+
|
|
|
|
| 191 |
if not selected_targets:
|
| 192 |
QtWidgets.QMessageBox.warning(
|
| 193 |
self,
|
|
|
|
| 196 |
)
|
| 197 |
return
|
| 198 |
|
| 199 |
+
# Store selected targets in global settings for persistence
|
| 200 |
+
self.global_settings._current_selected_targets = selected_targets
|
| 201 |
+
|
| 202 |
# Create and show generate library window
|
|
|
|
| 203 |
from controllers.GenerateLibraryController import GenerateLibraryController
|
| 204 |
generate_library_controller = GenerateLibraryController(
|
| 205 |
self.global_settings,
|
| 206 |
selected_targets
|
| 207 |
)
|
|
|
|
| 208 |
generate_library_controller.show()
|
| 209 |
|
| 210 |
except Exception as e:
|
|
|
|
| 214 |
"Error",
|
| 215 |
f"An error occurred while opening the generate library window: {str(e)}"
|
| 216 |
)
|
| 217 |
+
|
| 218 |
+
def _on_select_all_changed(self, state):
|
| 219 |
+
"""Handle select all checkbox state changes"""
|
| 220 |
+
try:
|
| 221 |
+
self.results_table.setUpdatesEnabled(False) # Disable updates for performance
|
| 222 |
+
if state == Qt.CheckState.Checked.value:
|
| 223 |
+
self.results_table.selectAll()
|
| 224 |
+
else:
|
| 225 |
+
self.results_table.clearSelection()
|
| 226 |
+
self.results_table.setUpdatesEnabled(True) # Re-enable updates
|
| 227 |
+
except Exception as e:
|
| 228 |
+
self.logger.error(f"Error in select all handler: {str(e)}")
|
|
@@ -24,7 +24,6 @@ class GenerateLibraryView(QMainWindow):
|
|
| 24 |
try:
|
| 25 |
# Load UI file
|
| 26 |
ui_file = os.path.join(self.global_settings.get_ui_dir_path(), 'generate_library.ui')
|
| 27 |
-
self.logger.debug(f"Loading UI file from: {ui_file}")
|
| 28 |
uic.loadUi(ui_file, self)
|
| 29 |
|
| 30 |
# Set window properties
|
|
@@ -47,6 +46,9 @@ class GenerateLibraryView(QMainWindow):
|
|
| 47 |
else:
|
| 48 |
self.ledFilePath.setText(default_path + "/")
|
| 49 |
|
|
|
|
|
|
|
|
|
|
| 50 |
# Center the window
|
| 51 |
self._center_window()
|
| 52 |
|
|
@@ -139,11 +141,19 @@ class GenerateLibraryView(QMainWindow):
|
|
| 139 |
)
|
| 140 |
}
|
| 141 |
|
| 142 |
-
if
|
|
|
|
|
|
|
|
|
|
| 143 |
try:
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
return settings
|
| 149 |
|
|
@@ -180,3 +190,12 @@ class GenerateLibraryView(QMainWindow):
|
|
| 180 |
"Success",
|
| 181 |
message
|
| 182 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
try:
|
| 25 |
# Load UI file
|
| 26 |
ui_file = os.path.join(self.global_settings.get_ui_dir_path(), 'generate_library.ui')
|
|
|
|
| 27 |
uic.loadUi(ui_file, self)
|
| 28 |
|
| 29 |
# Set window properties
|
|
|
|
| 46 |
else:
|
| 47 |
self.ledFilePath.setText(default_path + "/")
|
| 48 |
|
| 49 |
+
# Apply styles
|
| 50 |
+
self._set_styles()
|
| 51 |
+
|
| 52 |
# Center the window
|
| 53 |
self._center_window()
|
| 54 |
|
|
|
|
| 141 |
)
|
| 142 |
}
|
| 143 |
|
| 144 |
+
if settings['find_off_targets']:
|
| 145 |
+
max_off_target_score = self.cmbMaximumOffTargetScore.text().strip()
|
| 146 |
+
if not max_off_target_score:
|
| 147 |
+
raise ValueError("Please enter a maximum off-target score when Find Off Targets is enabled")
|
| 148 |
try:
|
| 149 |
+
score = float(max_off_target_score)
|
| 150 |
+
if not 0 <= score <= 0.5:
|
| 151 |
+
raise ValueError("Maximum off-target score must be between 0 and 0.5 (inclusive)")
|
| 152 |
+
settings['max_off_target_score'] = score
|
| 153 |
+
except ValueError as e:
|
| 154 |
+
if str(e).startswith("Maximum"):
|
| 155 |
+
raise
|
| 156 |
+
raise ValueError("Invalid maximum off-target score - please enter a valid number")
|
| 157 |
|
| 158 |
return settings
|
| 159 |
|
|
|
|
| 190 |
"Success",
|
| 191 |
message
|
| 192 |
)
|
| 193 |
+
|
| 194 |
+
def _set_styles(self):
|
| 195 |
+
"""Apply the global groupbox style"""
|
| 196 |
+
try:
|
| 197 |
+
style = self.global_settings.get_groupbox_style()
|
| 198 |
+
for groupbox in self.findChildren(QtWidgets.QGroupBox):
|
| 199 |
+
groupbox.setStyleSheet(style)
|
| 200 |
+
except Exception as e:
|
| 201 |
+
self.logger.error(f"Error setting styles: {str(e)}")
|
|
@@ -14,9 +14,34 @@ class HomeWindowView(QWidget):
|
|
| 14 |
try:
|
| 15 |
uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), "home_window.ui"), self)
|
| 16 |
self._init_ui_elements()
|
|
|
|
| 17 |
except Exception as e:
|
| 18 |
self._handle_init_error(e)
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
def _init_ui_elements(self) -> None:
|
| 21 |
# Create a main layout to hold everything
|
| 22 |
main_layout = QVBoxLayout(self)
|
|
@@ -39,15 +64,15 @@ class HomeWindowView(QWidget):
|
|
| 39 |
self._init_grpStep3()
|
| 40 |
|
| 41 |
# Connect to database manager signals
|
| 42 |
-
self.global_settings.db_manager.db_files_changed.connect(self._handle_db_files_changed)
|
| 43 |
-
self.global_settings.db_manager.db_state_changed.connect(self._handle_db_state_changed)
|
| 44 |
|
| 45 |
def _init_grpNavigationMenu(self) -> None:
|
| 46 |
self.push_button_new_genome = self._find_widget("pbtnNewGenome", QPushButton)
|
| 47 |
self.push_button_new_endonuclease = self._find_widget("pbtnNewEndonuclease", QPushButton)
|
| 48 |
self.push_button_multitargeting_analysis = self._find_widget("pbtnMultitargetingAnalysis", QPushButton)
|
| 49 |
self.push_button_population_analysis = self._find_widget("pbtnPopulationAnalysis", QPushButton)
|
| 50 |
-
self.push_button_combine_files = self._find_widget("pbtnCombineFiles", QPushButton)
|
| 51 |
|
| 52 |
def _init_grpStep1(self) -> None:
|
| 53 |
self.combo_box_organism = self._find_widget("cmbOrganism", QComboBox)
|
|
@@ -90,17 +115,47 @@ class HomeWindowView(QWidget):
|
|
| 90 |
show_error(self.global_settings, "Initialization Error", error_msg)
|
| 91 |
raise
|
| 92 |
|
| 93 |
-
def update_combo_box_endonuclease(self,
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
def update_combo_box_organism(self, organisms
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
def get_find_targets_input(self) -> dict:
|
| 106 |
current_annotation = self.combo_box_local_annotation_files.currentText()
|
|
@@ -125,32 +180,6 @@ class HomeWindowView(QWidget):
|
|
| 125 |
def get_annotation_file(self) -> str:
|
| 126 |
return self.combo_box_local_annotation_files.currentText()
|
| 127 |
|
| 128 |
-
def update_combo_box_annotation_files(self, files):
|
| 129 |
-
"""Update local annotation files combo box, excluding .index files"""
|
| 130 |
-
try:
|
| 131 |
-
# Clear existing items
|
| 132 |
-
self.combo_box_local_annotation_files.clear()
|
| 133 |
-
|
| 134 |
-
# Filter out .index files and ensure files are valid
|
| 135 |
-
filtered_files = [
|
| 136 |
-
f for f in files
|
| 137 |
-
if not f.endswith('.index') and f.strip()
|
| 138 |
-
]
|
| 139 |
-
|
| 140 |
-
# Add filtered files to combo box
|
| 141 |
-
if filtered_files:
|
| 142 |
-
self.combo_box_local_annotation_files.addItems(filtered_files)
|
| 143 |
-
# Set the first item as current
|
| 144 |
-
self.combo_box_local_annotation_files.setCurrentIndex(0)
|
| 145 |
-
# Emit the change signal to update the current annotation file
|
| 146 |
-
self._on_annotation_file_changed(self.combo_box_local_annotation_files.currentText())
|
| 147 |
-
self.logger.debug(f"Added {len(filtered_files)} local annotation files to combo box")
|
| 148 |
-
else:
|
| 149 |
-
self.logger.debug("No local annotation files found")
|
| 150 |
-
|
| 151 |
-
except Exception as e:
|
| 152 |
-
self.logger.error(f"Error updating local annotation files: {str(e)}")
|
| 153 |
-
|
| 154 |
def _on_annotation_file_changed(self, new_file):
|
| 155 |
"""Handle annotation file changes"""
|
| 156 |
try:
|
|
|
|
| 14 |
try:
|
| 15 |
uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), "home_window.ui"), self)
|
| 16 |
self._init_ui_elements()
|
| 17 |
+
self._set_styles()
|
| 18 |
except Exception as e:
|
| 19 |
self._handle_init_error(e)
|
| 20 |
|
| 21 |
+
def _set_styles(self) -> None:
|
| 22 |
+
"""Set the styles for the groupboxes"""
|
| 23 |
+
groupbox_style = """
|
| 24 |
+
QGroupBox:title {
|
| 25 |
+
subcontrol-origin: margin;
|
| 26 |
+
left: 10px;
|
| 27 |
+
padding: 0 5px 0 5px;
|
| 28 |
+
}
|
| 29 |
+
QGroupBox#grpStep1, QGroupBox#grpStep2, QGroupBox#grpStep3 {
|
| 30 |
+
border: 2px solid rgb(111,181,110);
|
| 31 |
+
border-radius: 9px;
|
| 32 |
+
margin-top: 10px;
|
| 33 |
+
}
|
| 34 |
+
QGroupBox#grpNavigationMenu {
|
| 35 |
+
border: 2px dashed rgb(88,89,91);
|
| 36 |
+
border-radius: 9px;
|
| 37 |
+
margin-top: 10px;
|
| 38 |
+
}
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
# Find and style all groupboxes
|
| 42 |
+
for child in self.findChildren(QtWidgets.QGroupBox):
|
| 43 |
+
child.setStyleSheet(groupbox_style)
|
| 44 |
+
|
| 45 |
def _init_ui_elements(self) -> None:
|
| 46 |
# Create a main layout to hold everything
|
| 47 |
main_layout = QVBoxLayout(self)
|
|
|
|
| 64 |
self._init_grpStep3()
|
| 65 |
|
| 66 |
# Connect to database manager signals
|
| 67 |
+
# self.global_settings.db_manager.db_files_changed.connect(self._handle_db_files_changed)
|
| 68 |
+
# self.global_settings.db_manager.db_state_changed.connect(self._handle_db_state_changed)
|
| 69 |
|
| 70 |
def _init_grpNavigationMenu(self) -> None:
|
| 71 |
self.push_button_new_genome = self._find_widget("pbtnNewGenome", QPushButton)
|
| 72 |
self.push_button_new_endonuclease = self._find_widget("pbtnNewEndonuclease", QPushButton)
|
| 73 |
self.push_button_multitargeting_analysis = self._find_widget("pbtnMultitargetingAnalysis", QPushButton)
|
| 74 |
self.push_button_population_analysis = self._find_widget("pbtnPopulationAnalysis", QPushButton)
|
| 75 |
+
# self.push_button_combine_files = self._find_widget("pbtnCombineFiles", QPushButton)
|
| 76 |
|
| 77 |
def _init_grpStep1(self) -> None:
|
| 78 |
self.combo_box_organism = self._find_widget("cmbOrganism", QComboBox)
|
|
|
|
| 115 |
show_error(self.global_settings, "Initialization Error", error_msg)
|
| 116 |
raise
|
| 117 |
|
| 118 |
+
def update_combo_box_endonuclease(self, endonucleases):
|
| 119 |
+
"""Update the endonuclease combo box"""
|
| 120 |
+
try:
|
| 121 |
+
current_text = self.combo_box_endonuclease.currentText()
|
| 122 |
+
self.combo_box_endonuclease.clear()
|
| 123 |
+
self.combo_box_endonuclease.addItems(endonucleases)
|
| 124 |
+
|
| 125 |
+
# Try to restore previous selection if it still exists
|
| 126 |
+
index = self.combo_box_endonuclease.findText(current_text)
|
| 127 |
+
if index >= 0:
|
| 128 |
+
self.combo_box_endonuclease.setCurrentIndex(index)
|
| 129 |
+
except Exception as e:
|
| 130 |
+
self.logger.error(f"Error updating endonuclease combo box: {str(e)}")
|
| 131 |
|
| 132 |
+
def update_combo_box_organism(self, organisms):
|
| 133 |
+
"""Update the organism combo box"""
|
| 134 |
+
try:
|
| 135 |
+
current_text = self.combo_box_organism.currentText()
|
| 136 |
+
self.combo_box_organism.clear()
|
| 137 |
+
self.combo_box_organism.addItems(organisms)
|
| 138 |
+
|
| 139 |
+
# Try to restore previous selection if it still exists
|
| 140 |
+
index = self.combo_box_organism.findText(current_text)
|
| 141 |
+
if index >= 0:
|
| 142 |
+
self.combo_box_organism.setCurrentIndex(index)
|
| 143 |
+
except Exception as e:
|
| 144 |
+
self.logger.error(f"Error updating organism combo box: {str(e)}")
|
| 145 |
|
| 146 |
+
def update_combo_box_annotation_files(self, files):
|
| 147 |
+
"""Update the annotation files combo box"""
|
| 148 |
+
try:
|
| 149 |
+
current_text = self.combo_box_local_annotation_files.currentText()
|
| 150 |
+
self.combo_box_local_annotation_files.clear()
|
| 151 |
+
self.combo_box_local_annotation_files.addItems(files)
|
| 152 |
+
|
| 153 |
+
# Try to restore previous selection if it still exists
|
| 154 |
+
index = self.combo_box_local_annotation_files.findText(current_text)
|
| 155 |
+
if index >= 0:
|
| 156 |
+
self.combo_box_local_annotation_files.setCurrentIndex(index)
|
| 157 |
+
except Exception as e:
|
| 158 |
+
self.logger.error(f"Error updating annotation files combo box: {str(e)}")
|
| 159 |
|
| 160 |
def get_find_targets_input(self) -> dict:
|
| 161 |
current_annotation = self.combo_box_local_annotation_files.currentText()
|
|
|
|
| 180 |
def get_annotation_file(self) -> str:
|
| 181 |
return self.combo_box_local_annotation_files.currentText()
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
def _on_annotation_file_changed(self, new_file):
|
| 184 |
"""Handle annotation file changes"""
|
| 185 |
try:
|
|
@@ -25,36 +25,18 @@ class LoadingDialog(QDialog):
|
|
| 25 |
layout.addWidget(self.progress_bar)
|
| 26 |
|
| 27 |
self.setLayout(layout)
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
| 30 |
self.center_on_parent()
|
| 31 |
|
| 32 |
def center_on_parent(self):
|
| 33 |
-
"""Center the dialog on the
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
if hasattr(parent, 'global_settings'):
|
| 39 |
-
main_window = parent.global_settings.main_window
|
| 40 |
-
elif hasattr(parent, 'settings'):
|
| 41 |
-
main_window = parent.settings.main_window
|
| 42 |
-
|
| 43 |
-
# Get geometry of the window to center on
|
| 44 |
-
if main_window and main_window.view:
|
| 45 |
-
geometry = main_window.view.geometry()
|
| 46 |
-
else:
|
| 47 |
-
geometry = parent.geometry()
|
| 48 |
-
|
| 49 |
-
# Calculate center position
|
| 50 |
-
x = geometry.x() + (geometry.width() - self.width()) // 2
|
| 51 |
-
y = geometry.y() + (geometry.height() - self.height()) // 2
|
| 52 |
-
|
| 53 |
-
# Ensure dialog stays within screen bounds
|
| 54 |
-
screen = QApplication.primaryScreen().geometry()
|
| 55 |
-
x = max(screen.left(), min(x, screen.right() - self.width()))
|
| 56 |
-
y = max(screen.top(), min(y, screen.bottom() - self.height()))
|
| 57 |
-
|
| 58 |
self.move(x, y)
|
| 59 |
|
| 60 |
def set_message(self, message, progress=None):
|
|
@@ -62,8 +44,6 @@ class LoadingDialog(QDialog):
|
|
| 62 |
if progress is not None:
|
| 63 |
self.progress_bar.setValue(progress)
|
| 64 |
self.label.setText(message)
|
| 65 |
-
|
| 66 |
-
# Recenter after updating message
|
| 67 |
self.center_on_parent()
|
| 68 |
|
| 69 |
def set_progress(self, value):
|
|
@@ -75,9 +55,4 @@ class LoadingDialog(QDialog):
|
|
| 75 |
"""Set indeterminate progress"""
|
| 76 |
self.progress_bar.setRange(0, 0)
|
| 77 |
self.label.setText("Loading...")
|
| 78 |
-
|
| 79 |
-
def showEvent(self, event):
|
| 80 |
-
"""Override show event to ensure dialog is centered when shown"""
|
| 81 |
-
super().showEvent(event)
|
| 82 |
-
self.center_on_parent()
|
| 83 |
|
|
|
|
| 25 |
layout.addWidget(self.progress_bar)
|
| 26 |
|
| 27 |
self.setLayout(layout)
|
| 28 |
+
|
| 29 |
+
def showEvent(self, event):
|
| 30 |
+
"""Override show event to ensure dialog is centered when shown"""
|
| 31 |
+
super().showEvent(event)
|
| 32 |
self.center_on_parent()
|
| 33 |
|
| 34 |
def center_on_parent(self):
|
| 35 |
+
"""Center the dialog on the parent window"""
|
| 36 |
+
if self.parent():
|
| 37 |
+
parent_geometry = self.parent().geometry()
|
| 38 |
+
x = parent_geometry.x() + (parent_geometry.width() - self.width()) // 2
|
| 39 |
+
y = parent_geometry.y() + (parent_geometry.height() - self.height()) // 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
self.move(x, y)
|
| 41 |
|
| 42 |
def set_message(self, message, progress=None):
|
|
|
|
| 44 |
if progress is not None:
|
| 45 |
self.progress_bar.setValue(progress)
|
| 46 |
self.label.setText(message)
|
|
|
|
|
|
|
| 47 |
self.center_on_parent()
|
| 48 |
|
| 49 |
def set_progress(self, value):
|
|
|
|
| 55 |
"""Set indeterminate progress"""
|
| 56 |
self.progress_bar.setRange(0, 0)
|
| 57 |
self.label.setText("Loading...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
|
@@ -25,9 +25,19 @@ class MultitargetingWindowView(QtWidgets.QMainWindow):
|
|
| 25 |
try:
|
| 26 |
uic.loadUi(self.settings.get_ui_dir_path() + '/multitargeting_window.ui', self)
|
| 27 |
self._init_ui_components()
|
|
|
|
| 28 |
except Exception as e:
|
| 29 |
show_error(self.settings, "Error initializing MultitargetingWindowView", str(e))
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def _init_ui_components(self):
|
| 32 |
self._init_grpSelectOrganism()
|
| 33 |
self._init_grpSeedAnalysis()
|
|
|
|
| 25 |
try:
|
| 26 |
uic.loadUi(self.settings.get_ui_dir_path() + '/multitargeting_window.ui', self)
|
| 27 |
self._init_ui_components()
|
| 28 |
+
self._set_styles()
|
| 29 |
except Exception as e:
|
| 30 |
show_error(self.settings, "Error initializing MultitargetingWindowView", str(e))
|
| 31 |
|
| 32 |
+
def _set_styles(self):
|
| 33 |
+
"""Apply the global groupbox style"""
|
| 34 |
+
try:
|
| 35 |
+
style = self.settings.get_groupbox_style()
|
| 36 |
+
for groupbox in self.findChildren(QtWidgets.QGroupBox):
|
| 37 |
+
groupbox.setStyleSheet(style)
|
| 38 |
+
except Exception as e:
|
| 39 |
+
self.logger.error(f"Error setting styles: {str(e)}")
|
| 40 |
+
|
| 41 |
def _init_ui_components(self):
|
| 42 |
self._init_grpSelectOrganism()
|
| 43 |
self._init_grpSeedAnalysis()
|
|
@@ -2,6 +2,7 @@ from PyQt6 import QtWidgets, QtGui, QtCore, uic
|
|
| 2 |
from PyQt6.QtWidgets import QWidget, QVBoxLayout
|
| 3 |
import os
|
| 4 |
from typing import Optional
|
|
|
|
| 5 |
|
| 6 |
class NCBIWindowView(QtWidgets.QMainWindow):
|
| 7 |
initialization_complete = QtCore.pyqtSignal() # New signal
|
|
@@ -18,27 +19,53 @@ class NCBIWindowView(QtWidgets.QMainWindow):
|
|
| 18 |
def _setup_basic_ui(self):
|
| 19 |
"""Initial minimal setup to show the window quickly"""
|
| 20 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), "ncbi.ui"), self)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
QtCore.QTimer.singleShot(100, self._complete_initialization)
|
| 24 |
|
|
|
|
| 25 |
except Exception as e:
|
| 26 |
self.logger.error(f"Error in basic UI setup: {str(e)}")
|
| 27 |
raise
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def _complete_initialization(self):
|
| 30 |
"""Complete the full initialization of UI components"""
|
| 31 |
try:
|
| 32 |
if self._is_initialized:
|
| 33 |
return
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
# Initialize all UI components
|
|
|
|
| 36 |
self._init_ui_components()
|
|
|
|
| 37 |
|
| 38 |
self._is_initialized = True
|
| 39 |
|
| 40 |
# Emit signal after everything is initialized
|
| 41 |
self.initialization_complete.emit()
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
except Exception as e:
|
| 44 |
self.logger.error(f"Error in complete initialization: {str(e)}")
|
|
@@ -47,9 +74,20 @@ class NCBIWindowView(QtWidgets.QMainWindow):
|
|
| 47 |
def _init_ui_components(self) -> None:
|
| 48 |
"""Initialize all UI components at once instead of using timers"""
|
| 49 |
try:
|
|
|
|
|
|
|
|
|
|
| 50 |
self._init_grpStep1()
|
|
|
|
|
|
|
|
|
|
| 51 |
self._init_grpStep2()
|
|
|
|
|
|
|
|
|
|
| 52 |
self._init_grpStep3()
|
|
|
|
|
|
|
| 53 |
except Exception as e:
|
| 54 |
self.logger.error(f"Error in _init_ui_components: {str(e)}")
|
| 55 |
raise
|
|
|
|
| 2 |
from PyQt6.QtWidgets import QWidget, QVBoxLayout
|
| 3 |
import os
|
| 4 |
from typing import Optional
|
| 5 |
+
import time
|
| 6 |
|
| 7 |
class NCBIWindowView(QtWidgets.QMainWindow):
|
| 8 |
initialization_complete = QtCore.pyqtSignal() # New signal
|
|
|
|
| 19 |
def _setup_basic_ui(self):
|
| 20 |
"""Initial minimal setup to show the window quickly"""
|
| 21 |
try:
|
| 22 |
+
start_time = time.time()
|
| 23 |
+
self.logger.debug("Starting basic UI setup")
|
| 24 |
+
|
| 25 |
+
ui_load_start = time.time()
|
| 26 |
uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), "ncbi.ui"), self)
|
| 27 |
+
self.logger.debug(f"Loading UI file took: {time.time() - ui_load_start:.2f} seconds")
|
| 28 |
+
|
| 29 |
+
# Apply styles immediately for better visual experience
|
| 30 |
+
self._set_styles()
|
| 31 |
|
| 32 |
QtCore.QTimer.singleShot(100, self._complete_initialization)
|
| 33 |
|
| 34 |
+
self.logger.debug(f"Basic UI setup completed in: {time.time() - start_time:.2f} seconds")
|
| 35 |
except Exception as e:
|
| 36 |
self.logger.error(f"Error in basic UI setup: {str(e)}")
|
| 37 |
raise
|
| 38 |
|
| 39 |
+
def _set_styles(self):
|
| 40 |
+
"""Apply the global groupbox style"""
|
| 41 |
+
try:
|
| 42 |
+
style = self.settings.get_groupbox_style()
|
| 43 |
+
for groupbox in self.findChildren(QtWidgets.QGroupBox):
|
| 44 |
+
groupbox.setStyleSheet(style)
|
| 45 |
+
except Exception as e:
|
| 46 |
+
self.logger.error(f"Error setting styles: {str(e)}")
|
| 47 |
+
|
| 48 |
def _complete_initialization(self):
|
| 49 |
"""Complete the full initialization of UI components"""
|
| 50 |
try:
|
| 51 |
if self._is_initialized:
|
| 52 |
return
|
| 53 |
|
| 54 |
+
start_time = time.time()
|
| 55 |
+
self.logger.debug("Starting complete initialization")
|
| 56 |
+
|
| 57 |
# Initialize all UI components
|
| 58 |
+
components_start = time.time()
|
| 59 |
self._init_ui_components()
|
| 60 |
+
self.logger.debug(f"UI components initialization took: {time.time() - components_start:.2f} seconds")
|
| 61 |
|
| 62 |
self._is_initialized = True
|
| 63 |
|
| 64 |
# Emit signal after everything is initialized
|
| 65 |
self.initialization_complete.emit()
|
| 66 |
+
self.logger.debug("Emitted initialization complete signal")
|
| 67 |
+
|
| 68 |
+
self.logger.debug(f"Complete initialization finished in: {time.time() - start_time:.2f} seconds")
|
| 69 |
|
| 70 |
except Exception as e:
|
| 71 |
self.logger.error(f"Error in complete initialization: {str(e)}")
|
|
|
|
| 74 |
def _init_ui_components(self) -> None:
|
| 75 |
"""Initialize all UI components at once instead of using timers"""
|
| 76 |
try:
|
| 77 |
+
self.logger.debug("Starting UI components initialization")
|
| 78 |
+
|
| 79 |
+
step1_start = time.time()
|
| 80 |
self._init_grpStep1()
|
| 81 |
+
self.logger.debug(f"Step 1 initialization took: {time.time() - step1_start:.2f} seconds")
|
| 82 |
+
|
| 83 |
+
step2_start = time.time()
|
| 84 |
self._init_grpStep2()
|
| 85 |
+
self.logger.debug(f"Step 2 initialization took: {time.time() - step2_start:.2f} seconds")
|
| 86 |
+
|
| 87 |
+
step3_start = time.time()
|
| 88 |
self._init_grpStep3()
|
| 89 |
+
self.logger.debug(f"Step 3 initialization took: {time.time() - step3_start:.2f} seconds")
|
| 90 |
+
|
| 91 |
except Exception as e:
|
| 92 |
self.logger.error(f"Error in _init_ui_components: {str(e)}")
|
| 93 |
raise
|
|
@@ -99,16 +99,6 @@ class NewEndonucleaseView(QtWidgets.QMainWindow):
|
|
| 99 |
image: none;
|
| 100 |
border: none;
|
| 101 |
}}
|
| 102 |
-
QGroupBox {{
|
| 103 |
-
border: 1px solid {theme['button_border_color']};
|
| 104 |
-
margin-top: 1em;
|
| 105 |
-
padding-top: 0.5em;
|
| 106 |
-
}}
|
| 107 |
-
QGroupBox::title {{
|
| 108 |
-
subcontrol-origin: margin;
|
| 109 |
-
left: 10px;
|
| 110 |
-
padding: 0 3px 0 3px;
|
| 111 |
-
}}
|
| 112 |
QRadioButton {{
|
| 113 |
color: {theme['fg_color']};
|
| 114 |
}}
|
|
@@ -132,19 +122,32 @@ class NewEndonucleaseView(QtWidgets.QMainWindow):
|
|
| 132 |
""")
|
| 133 |
|
| 134 |
def showEvent(self, event):
|
| 135 |
-
"""Override showEvent to apply theme when window is shown"""
|
| 136 |
super().showEvent(event)
|
| 137 |
-
self.
|
| 138 |
|
| 139 |
def init_ui(self):
|
| 140 |
try:
|
| 141 |
uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), 'new_endonuclease_window.ui'), self)
|
| 142 |
-
|
| 143 |
self._init_ui_components()
|
|
|
|
| 144 |
self.disable_form_elements()
|
| 145 |
except Exception as e:
|
| 146 |
show_error(self.settings, "Error initializing NewEndonucleaseView", str(e))
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
def _init_ui_components(self):
|
| 149 |
self.combo_box_select_endonuclease = self._find_widget('cmbSelectEndonuclease', QtWidgets.QComboBox)
|
| 150 |
|
|
|
|
| 99 |
image: none;
|
| 100 |
border: none;
|
| 101 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
QRadioButton {{
|
| 103 |
color: {theme['fg_color']};
|
| 104 |
}}
|
|
|
|
| 122 |
""")
|
| 123 |
|
| 124 |
def showEvent(self, event):
|
| 125 |
+
"""Override showEvent to apply theme and styles when window is shown"""
|
| 126 |
super().showEvent(event)
|
| 127 |
+
self._set_styles()
|
| 128 |
|
| 129 |
def init_ui(self):
|
| 130 |
try:
|
| 131 |
uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), 'new_endonuclease_window.ui'), self)
|
|
|
|
| 132 |
self._init_ui_components()
|
| 133 |
+
self._set_styles() # Add style initialization
|
| 134 |
self.disable_form_elements()
|
| 135 |
except Exception as e:
|
| 136 |
show_error(self.settings, "Error initializing NewEndonucleaseView", str(e))
|
| 137 |
|
| 138 |
+
def _set_styles(self):
|
| 139 |
+
"""Apply the global groupbox style and theme"""
|
| 140 |
+
try:
|
| 141 |
+
# Apply global groupbox style
|
| 142 |
+
style = self.settings.get_groupbox_style()
|
| 143 |
+
for groupbox in self.findChildren(QtWidgets.QGroupBox):
|
| 144 |
+
groupbox.setStyleSheet(style)
|
| 145 |
+
|
| 146 |
+
# Apply theme
|
| 147 |
+
self.apply_theme()
|
| 148 |
+
except Exception as e:
|
| 149 |
+
self.logger.error(f"Error setting styles: {str(e)}")
|
| 150 |
+
|
| 151 |
def _init_ui_components(self):
|
| 152 |
self.combo_box_select_endonuclease = self._find_widget('cmbSelectEndonuclease', QtWidgets.QComboBox)
|
| 153 |
|
|
@@ -18,23 +18,17 @@ class NewGenomeWindowView(QtWidgets.QMainWindow):
|
|
| 18 |
|
| 19 |
def _init_ui(self):
|
| 20 |
uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), 'new_genome_window.ui'), self)
|
| 21 |
-
# self.set_styles()
|
| 22 |
-
|
| 23 |
self._init_ui_components()
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
self.Step1.setStyleSheet(groupbox_style)
|
| 36 |
-
self.Step2.setStyleSheet(groupbox_style.replace("Step1","Step2"))
|
| 37 |
-
self.Step3.setStyleSheet(groupbox_style.replace("Step1","Step3"))
|
| 38 |
|
| 39 |
def _init_ui_components(self):
|
| 40 |
self._init_grpStep1()
|
|
|
|
| 18 |
|
| 19 |
def _init_ui(self):
|
| 20 |
uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), 'new_genome_window.ui'), self)
|
|
|
|
|
|
|
| 21 |
self._init_ui_components()
|
| 22 |
+
self._set_styles()
|
| 23 |
+
|
| 24 |
+
def _set_styles(self):
|
| 25 |
+
"""Apply the global groupbox style"""
|
| 26 |
+
try:
|
| 27 |
+
style = self.global_settings.get_groupbox_style()
|
| 28 |
+
for groupbox in self.findChildren(QtWidgets.QGroupBox):
|
| 29 |
+
groupbox.setStyleSheet(style)
|
| 30 |
+
except Exception as e:
|
| 31 |
+
self.logger.error(f"Error setting styles: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
def _init_ui_components(self):
|
| 34 |
self._init_grpStep1()
|
|
@@ -24,6 +24,7 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 24 |
try:
|
| 25 |
uic.loadUi(self.settings.get_ui_dir_path() + '/population_analysis.ui', self)
|
| 26 |
self._init_ui_components()
|
|
|
|
| 27 |
except Exception as e:
|
| 28 |
show_error(self.settings, "Error initializing PopulationAnalysisWindowView", str(e))
|
| 29 |
|
|
@@ -35,8 +36,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 35 |
|
| 36 |
def _init_grpSelectOrganisms(self):
|
| 37 |
try:
|
| 38 |
-
self.logger.debug("Starting _init_grpSelectOrganisms")
|
| 39 |
-
|
| 40 |
self.combo_box_endonuclease = self._find_widget('cmbEndonuclease', QtWidgets.QComboBox)
|
| 41 |
self.table_organism = self._find_widget('tblOrganism', QtWidgets.QTableWidget)
|
| 42 |
self.push_button_analyze_organism = self._find_widget('pbtnAnalyzeOrganism', QtWidgets.QPushButton)
|
|
@@ -46,9 +45,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 46 |
self.tab_shared_seed_heatmap = self._find_widget('tabSharedSeedHeatmap', QtWidgets.QWidget)
|
| 47 |
self.heatmap_seed = self._find_widget('heatmapSeed', QtWidgets.QWidget)
|
| 48 |
|
| 49 |
-
self.logger.debug(f"Tab widget found: {self.tab_widget_shared_seeds_heatmap is not None}")
|
| 50 |
-
self.logger.debug(f"Heatmap widget found: {self.heatmap_seed is not None}")
|
| 51 |
-
|
| 52 |
# Create layout for heatmap
|
| 53 |
self.colormap_layout = QtWidgets.QVBoxLayout(self.heatmap_seed)
|
| 54 |
self.colormap_layout.setContentsMargins(0, 0, 0, 0)
|
|
@@ -58,7 +54,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 58 |
self.colormap_layout.addWidget(self.colormap_canvas)
|
| 59 |
|
| 60 |
# Set up the organism table
|
| 61 |
-
self.logger.debug("Setting up organism table")
|
| 62 |
self.table_organism.setColumnCount(1)
|
| 63 |
self.table_organism.setShowGrid(False)
|
| 64 |
self.table_organism.setHorizontalHeaderLabels(["Organism"])
|
|
@@ -68,8 +63,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 68 |
self.table_organism.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
| 69 |
self.table_organism.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
| 70 |
|
| 71 |
-
self.logger.debug("Completed _init_grpSelectOrganisms")
|
| 72 |
-
|
| 73 |
except Exception as e:
|
| 74 |
self.logger.error(f"Error in _init_grpSelectOrganisms: {str(e)}")
|
| 75 |
self.logger.exception("Full traceback:")
|
|
@@ -138,7 +131,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 138 |
def update_shared_seeds_table(self, seed_data):
|
| 139 |
self.table_seed.setRowCount(len(seed_data))
|
| 140 |
for row, data in enumerate(seed_data):
|
| 141 |
-
print(data)
|
| 142 |
for col, value in enumerate(data):
|
| 143 |
item = QtWidgets.QTableWidgetItem(str(value))
|
| 144 |
item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
|
@@ -148,7 +140,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 148 |
def update_loc_finder_table(self, loc_data):
|
| 149 |
self.table_locations.setRowCount(len(loc_data))
|
| 150 |
for row, data in enumerate(loc_data):
|
| 151 |
-
print(data)
|
| 152 |
for col, key in enumerate(['seed', 'sequence', 'organism', 'chromosome', 'location']):
|
| 153 |
item = QtWidgets.QTableWidgetItem(str(data[key]))
|
| 154 |
item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
|
@@ -253,35 +244,22 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 253 |
self.table_seed.setRowCount(0)
|
| 254 |
|
| 255 |
def clear_loc_finder_table(self):
|
| 256 |
-
self.
|
| 257 |
|
| 258 |
def update_endo_dropdown(self, endos):
|
| 259 |
"""Update the endonuclease dropdown with the provided options"""
|
| 260 |
try:
|
| 261 |
-
self.logger.info("Starting update_endo_dropdown")
|
| 262 |
-
self.logger.debug(f"Received endos: {endos}")
|
| 263 |
-
|
| 264 |
-
print(self.combo_box_endonuclease)
|
| 265 |
-
|
| 266 |
-
# if not self.combo_box_endonuclease:
|
| 267 |
-
# self.logger.error("combo_box_endonuclease is None")
|
| 268 |
-
# return
|
| 269 |
-
|
| 270 |
self.combo_box_endonuclease.clear()
|
| 271 |
self.combo_box_endonuclease.addItems(endos)
|
| 272 |
-
|
| 273 |
-
self.logger.info(f"Updated endonuclease dropdown with {len(endos)} options")
|
| 274 |
-
self.logger.debug(f"Current items in dropdown: {[self.combo_box_endonuclease.itemText(i) for i in range(self.combo_box_endonuclease.count())]}")
|
| 275 |
except Exception as e:
|
| 276 |
self.logger.error(f"Error updating endonuclease dropdown: {str(e)}")
|
| 277 |
-
self.logger.exception("Full traceback:")
|
| 278 |
show_error(self.settings, "Error updating endonuclease dropdown", str(e))
|
| 279 |
|
| 280 |
def sort_table2(self, column):
|
| 281 |
self.table_seed.sortItems(column)
|
| 282 |
|
| 283 |
def sort_loc_finder_table(self, column):
|
| 284 |
-
self.
|
| 285 |
|
| 286 |
def _on_theme_changed(self, theme):
|
| 287 |
"""Handle theme changes by updating the plot"""
|
|
@@ -307,6 +285,15 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
|
|
| 307 |
except Exception as e:
|
| 308 |
self.logger.error(f"Error updating plot theme: {str(e)}")
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
class MplCanvas(FigureCanvasQTAgg):
|
| 311 |
def __init__(self, parent=None, width=8, height=6, dpi=100):
|
| 312 |
self.fig = Figure(figsize=(width, height), dpi=dpi)
|
|
|
|
| 24 |
try:
|
| 25 |
uic.loadUi(self.settings.get_ui_dir_path() + '/population_analysis.ui', self)
|
| 26 |
self._init_ui_components()
|
| 27 |
+
self._set_styles()
|
| 28 |
except Exception as e:
|
| 29 |
show_error(self.settings, "Error initializing PopulationAnalysisWindowView", str(e))
|
| 30 |
|
|
|
|
| 36 |
|
| 37 |
def _init_grpSelectOrganisms(self):
|
| 38 |
try:
|
|
|
|
|
|
|
| 39 |
self.combo_box_endonuclease = self._find_widget('cmbEndonuclease', QtWidgets.QComboBox)
|
| 40 |
self.table_organism = self._find_widget('tblOrganism', QtWidgets.QTableWidget)
|
| 41 |
self.push_button_analyze_organism = self._find_widget('pbtnAnalyzeOrganism', QtWidgets.QPushButton)
|
|
|
|
| 45 |
self.tab_shared_seed_heatmap = self._find_widget('tabSharedSeedHeatmap', QtWidgets.QWidget)
|
| 46 |
self.heatmap_seed = self._find_widget('heatmapSeed', QtWidgets.QWidget)
|
| 47 |
|
|
|
|
|
|
|
|
|
|
| 48 |
# Create layout for heatmap
|
| 49 |
self.colormap_layout = QtWidgets.QVBoxLayout(self.heatmap_seed)
|
| 50 |
self.colormap_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
| 54 |
self.colormap_layout.addWidget(self.colormap_canvas)
|
| 55 |
|
| 56 |
# Set up the organism table
|
|
|
|
| 57 |
self.table_organism.setColumnCount(1)
|
| 58 |
self.table_organism.setShowGrid(False)
|
| 59 |
self.table_organism.setHorizontalHeaderLabels(["Organism"])
|
|
|
|
| 63 |
self.table_organism.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
| 64 |
self.table_organism.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
| 65 |
|
|
|
|
|
|
|
| 66 |
except Exception as e:
|
| 67 |
self.logger.error(f"Error in _init_grpSelectOrganisms: {str(e)}")
|
| 68 |
self.logger.exception("Full traceback:")
|
|
|
|
| 131 |
def update_shared_seeds_table(self, seed_data):
|
| 132 |
self.table_seed.setRowCount(len(seed_data))
|
| 133 |
for row, data in enumerate(seed_data):
|
|
|
|
| 134 |
for col, value in enumerate(data):
|
| 135 |
item = QtWidgets.QTableWidgetItem(str(value))
|
| 136 |
item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
|
|
|
| 140 |
def update_loc_finder_table(self, loc_data):
|
| 141 |
self.table_locations.setRowCount(len(loc_data))
|
| 142 |
for row, data in enumerate(loc_data):
|
|
|
|
| 143 |
for col, key in enumerate(['seed', 'sequence', 'organism', 'chromosome', 'location']):
|
| 144 |
item = QtWidgets.QTableWidgetItem(str(data[key]))
|
| 145 |
item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
|
|
|
| 244 |
self.table_seed.setRowCount(0)
|
| 245 |
|
| 246 |
def clear_loc_finder_table(self):
|
| 247 |
+
self.table_locations.setRowCount(0)
|
| 248 |
|
| 249 |
def update_endo_dropdown(self, endos):
|
| 250 |
"""Update the endonuclease dropdown with the provided options"""
|
| 251 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
self.combo_box_endonuclease.clear()
|
| 253 |
self.combo_box_endonuclease.addItems(endos)
|
|
|
|
|
|
|
|
|
|
| 254 |
except Exception as e:
|
| 255 |
self.logger.error(f"Error updating endonuclease dropdown: {str(e)}")
|
|
|
|
| 256 |
show_error(self.settings, "Error updating endonuclease dropdown", str(e))
|
| 257 |
|
| 258 |
def sort_table2(self, column):
|
| 259 |
self.table_seed.sortItems(column)
|
| 260 |
|
| 261 |
def sort_loc_finder_table(self, column):
|
| 262 |
+
self.table_locations.sortItems(column)
|
| 263 |
|
| 264 |
def _on_theme_changed(self, theme):
|
| 265 |
"""Handle theme changes by updating the plot"""
|
|
|
|
| 285 |
except Exception as e:
|
| 286 |
self.logger.error(f"Error updating plot theme: {str(e)}")
|
| 287 |
|
| 288 |
+
def _set_styles(self):
|
| 289 |
+
"""Apply the global groupbox style"""
|
| 290 |
+
try:
|
| 291 |
+
style = self.settings.get_groupbox_style()
|
| 292 |
+
for groupbox in self.findChildren(QtWidgets.QGroupBox):
|
| 293 |
+
groupbox.setStyleSheet(style)
|
| 294 |
+
except Exception as e:
|
| 295 |
+
self.logger.error(f"Error setting styles: {str(e)}")
|
| 296 |
+
|
| 297 |
class MplCanvas(FigureCanvasQTAgg):
|
| 298 |
def __init__(self, parent=None, width=8, height=6, dpi=100):
|
| 299 |
self.fig = Figure(figsize=(width, height), dpi=dpi)
|
|
@@ -75,9 +75,10 @@ class StartupWindowView(QtWidgets.QMainWindow):
|
|
| 75 |
if is_valid:
|
| 76 |
self.label_db_status.hide()
|
| 77 |
self.push_button_go_to_home_or_new_genome.setText("Go to Home")
|
|
|
|
| 78 |
else:
|
| 79 |
self.label_db_status.setText(message)
|
| 80 |
self.label_db_status.show()
|
| 81 |
self.label_db_status.setStyleSheet("color: red;")
|
| 82 |
self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
|
| 83 |
-
|
|
|
|
| 75 |
if is_valid:
|
| 76 |
self.label_db_status.hide()
|
| 77 |
self.push_button_go_to_home_or_new_genome.setText("Go to Home")
|
| 78 |
+
self.push_button_go_to_home_or_new_genome.setEnabled(True)
|
| 79 |
else:
|
| 80 |
self.label_db_status.setText(message)
|
| 81 |
self.label_db_status.show()
|
| 82 |
self.label_db_status.setStyleSheet("color: red;")
|
| 83 |
self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
|
| 84 |
+
self.push_button_go_to_home_or_new_genome.setEnabled(True)
|
|
@@ -1,11 +1,12 @@
|
|
| 1 |
from typing import Optional
|
| 2 |
from PyQt6 import QtWidgets, uic
|
| 3 |
-
from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
|
| 4 |
-
from PyQt6.QtGui import QTextDocument
|
| 5 |
-
from PyQt6.QtCore import Qt, pyqtSignal
|
| 6 |
from utils.ui import show_error
|
| 7 |
import traceback
|
| 8 |
-
from views.
|
|
|
|
| 9 |
|
| 10 |
class ViewTargetsView(QtWidgets.QMainWindow):
|
| 11 |
# Define the signal
|
|
@@ -21,10 +22,21 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 21 |
def init_ui(self):
|
| 22 |
try:
|
| 23 |
uic.loadUi(self.settings.get_ui_dir_path() + '/view_targets.ui', self)
|
|
|
|
| 24 |
self._init_ui_components()
|
|
|
|
| 25 |
except Exception as e:
|
| 26 |
show_error(self.settings, "Error initializing ViewTargetsView", str(e))
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
def _init_ui_components(self):
|
| 29 |
self._init_grpGuideViewer()
|
| 30 |
self._init_grpGuideAnalysis()
|
|
@@ -75,6 +87,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 75 |
self.push_button_cotargeting = self._find_widget('pbtnCoTargeting', QtWidgets.QPushButton)
|
| 76 |
|
| 77 |
def _init_grpGeneViewer(self):
|
|
|
|
| 78 |
self.push_button_highlight_guides = self._find_widget('pbtnHighlightGuides', QtWidgets.QPushButton)
|
| 79 |
self.push_button_clear_guides = self._find_widget('pbtnClearGuides', QtWidgets.QPushButton)
|
| 80 |
self.line_edit_start_location = self._find_widget('ledStartLocation', QtWidgets.QLineEdit)
|
|
@@ -82,31 +95,47 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 82 |
self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
|
| 83 |
self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
|
| 84 |
self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
|
|
|
|
| 85 |
self.check_box_view_exons_only = self._find_widget('chkViewExonsOnly', QtWidgets.QCheckBox)
|
| 86 |
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 112 |
widget = self.findChild(widget_type, name)
|
|
@@ -114,10 +143,9 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 114 |
self.logger.warning(f"Widget '{name}' not found in UI file.")
|
| 115 |
return widget
|
| 116 |
|
| 117 |
-
def display_guides_in_table(self, guides):
|
| 118 |
try:
|
| 119 |
self._all_guides = guides
|
| 120 |
-
|
| 121 |
selected_text = self.combo_box_gene.currentText()
|
| 122 |
|
| 123 |
# First filter by position/feature
|
|
@@ -138,6 +166,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 138 |
filtered_guides.append(guide)
|
| 139 |
else:
|
| 140 |
filtered_guides = self._all_guides
|
|
|
|
| 141 |
|
| 142 |
# Apply additional filters
|
| 143 |
final_guides = []
|
|
@@ -157,13 +186,13 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 157 |
|
| 158 |
# Update table with new guides
|
| 159 |
total_rows = len(final_guides)
|
| 160 |
-
self.logger.debug(f"Processing {total_rows} rows for display after filtering")
|
| 161 |
|
| 162 |
-
# Completely freeze UI
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
| 167 |
|
| 168 |
try:
|
| 169 |
# Clear and resize table
|
|
@@ -237,11 +266,12 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 237 |
guide_viewer_group.setMinimumWidth(essential_columns_width + 50) # Add some padding for scrollbar
|
| 238 |
|
| 239 |
finally:
|
| 240 |
-
# Re-enable UI
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
| 245 |
|
| 246 |
except Exception as e:
|
| 247 |
self.logger.error(f"Error in display_guides: {str(e)}")
|
|
@@ -390,9 +420,6 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 390 |
# Clear existing items efficiently
|
| 391 |
self.combo_box_gene.clear()
|
| 392 |
|
| 393 |
-
# Debug logging
|
| 394 |
-
self.logger.debug(f"Received {len(genes)} genes")
|
| 395 |
-
|
| 396 |
# Use a set to ensure uniqueness
|
| 397 |
unique_genes = list(set(genes))
|
| 398 |
|
|
@@ -404,8 +431,6 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 404 |
# Set first item without triggering updates
|
| 405 |
if self.combo_box_gene.count() > 0:
|
| 406 |
self.combo_box_gene.setCurrentIndex(0)
|
| 407 |
-
|
| 408 |
-
self.logger.debug(f"Added {len(unique_genes)} unique genes to combo box")
|
| 409 |
|
| 410 |
# Re-enable UI updates
|
| 411 |
self.combo_box_gene.setUpdatesEnabled(True)
|
|
@@ -429,22 +454,26 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 429 |
|
| 430 |
def update_gene_viewer(self, sequence, features=None):
|
| 431 |
"""Update both text editor and DNA feature viewer"""
|
| 432 |
-
# Update text editor
|
| 433 |
-
self.text_edit_gene_viewer.clear()
|
| 434 |
-
doc = QTextDocument()
|
| 435 |
-
doc.setHtml(sequence)
|
| 436 |
-
self.text_edit_gene_viewer.setDocument(doc)
|
| 437 |
-
|
| 438 |
-
# Get start position from line edit
|
| 439 |
try:
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
def select_all_guides(self, select):
|
| 450 |
for row in range(self.table_guides.rowCount()):
|
|
@@ -559,40 +588,256 @@ class ViewTargetsView(QtWidgets.QMainWindow):
|
|
| 559 |
self.logger.error(f"Error showing details: {str(e)}")
|
| 560 |
show_error(self.settings, "Error showing details", str(e))
|
| 561 |
|
| 562 |
-
def
|
| 563 |
-
"""
|
| 564 |
-
self.
|
| 565 |
-
self.line_edit_stop_location.setText(str(end))
|
| 566 |
|
| 567 |
-
def
|
| 568 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
try:
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
|
|
|
|
|
|
| 573 |
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
|
| 577 |
-
|
| 578 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
|
| 592 |
except Exception as e:
|
| 593 |
-
self
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
"""Clear highlights in viewer"""
|
| 598 |
-
self.dna_feature_viewer.sequence_viewer.clear_highlights()
|
|
|
|
| 1 |
from typing import Optional
|
| 2 |
from PyQt6 import QtWidgets, uic
|
| 3 |
+
from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView, QMessageBox, QApplication, QDialog
|
| 4 |
+
from PyQt6.QtGui import QTextDocument, QKeySequence, QColor
|
| 5 |
+
from PyQt6.QtCore import Qt, pyqtSignal, QEvent
|
| 6 |
from utils.ui import show_error
|
| 7 |
import traceback
|
| 8 |
+
from views.dna_viewer.dna_feature_viewer import DNAFeatureViewer
|
| 9 |
+
from .dialogs.base_insertion_dialog import BaseInsertionDialog
|
| 10 |
|
| 11 |
class ViewTargetsView(QtWidgets.QMainWindow):
|
| 12 |
# Define the signal
|
|
|
|
| 22 |
def init_ui(self):
|
| 23 |
try:
|
| 24 |
uic.loadUi(self.settings.get_ui_dir_path() + '/view_targets.ui', self)
|
| 25 |
+
self.dna_feature_viewer = DNAFeatureViewer(parent=self)
|
| 26 |
self._init_ui_components()
|
| 27 |
+
self._set_styles() # Add style initialization
|
| 28 |
except Exception as e:
|
| 29 |
show_error(self.settings, "Error initializing ViewTargetsView", str(e))
|
| 30 |
|
| 31 |
+
def _set_styles(self):
|
| 32 |
+
"""Apply the global groupbox style"""
|
| 33 |
+
try:
|
| 34 |
+
style = self.settings.get_groupbox_style()
|
| 35 |
+
for groupbox in self.findChildren(QtWidgets.QGroupBox):
|
| 36 |
+
groupbox.setStyleSheet(style)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
self.logger.error(f"Error setting styles: {str(e)}")
|
| 39 |
+
|
| 40 |
def _init_ui_components(self):
|
| 41 |
self._init_grpGuideViewer()
|
| 42 |
self._init_grpGuideAnalysis()
|
|
|
|
| 87 |
self.push_button_cotargeting = self._find_widget('pbtnCoTargeting', QtWidgets.QPushButton)
|
| 88 |
|
| 89 |
def _init_grpGeneViewer(self):
|
| 90 |
+
"""Initialize gene viewer group"""
|
| 91 |
self.push_button_highlight_guides = self._find_widget('pbtnHighlightGuides', QtWidgets.QPushButton)
|
| 92 |
self.push_button_clear_guides = self._find_widget('pbtnClearGuides', QtWidgets.QPushButton)
|
| 93 |
self.line_edit_start_location = self._find_widget('ledStartLocation', QtWidgets.QLineEdit)
|
|
|
|
| 95 |
self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
|
| 96 |
self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
|
| 97 |
self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
|
| 98 |
+
self.label_sequence_legend = self._find_widget('lblSequenceLegend', QtWidgets.QLabel)
|
| 99 |
self.check_box_view_exons_only = self._find_widget('chkViewExonsOnly', QtWidgets.QCheckBox)
|
| 100 |
|
| 101 |
+
# Hide the text editor since we're using the DNA feature viewer
|
| 102 |
+
self.text_edit_gene_viewer.hide()
|
| 103 |
|
| 104 |
+
try:
|
| 105 |
+
# Get the layout of the gene viewer group
|
| 106 |
+
gene_viewer_group = self.findChild(QtWidgets.QGroupBox, 'grpGeneViewer')
|
| 107 |
+
gene_viewer_layout = gene_viewer_group.layout()
|
| 108 |
+
|
| 109 |
+
# Find the row index of the text editor
|
| 110 |
+
text_editor_row = -1
|
| 111 |
+
for i in range(gene_viewer_layout.rowCount()):
|
| 112 |
+
item = gene_viewer_layout.itemAtPosition(i, 0)
|
| 113 |
+
if item and item.widget() == self.text_edit_gene_viewer:
|
| 114 |
+
text_editor_row = i
|
| 115 |
+
break
|
| 116 |
+
|
| 117 |
+
if text_editor_row != -1:
|
| 118 |
+
# Remove the text editor from the layout
|
| 119 |
+
gene_viewer_layout.removeWidget(self.text_edit_gene_viewer)
|
| 120 |
+
# Add DNA feature viewer in its place
|
| 121 |
+
gene_viewer_layout.addWidget(self.dna_feature_viewer, text_editor_row, 0, 1, -1)
|
| 122 |
+
|
| 123 |
+
# Set size policy to make the DNA viewer expand
|
| 124 |
+
self.dna_feature_viewer.setSizePolicy(
|
| 125 |
+
QtWidgets.QSizePolicy.Policy.Expanding,
|
| 126 |
+
QtWidgets.QSizePolicy.Policy.Expanding
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
self.dna_feature_viewer.view.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
| 130 |
+
self.dna_feature_viewer.view.installEventFilter(self)
|
| 131 |
+
self.dna_feature_viewer.installEventFilter(self)
|
| 132 |
+
|
| 133 |
+
# Store current sequence for editing
|
| 134 |
+
self._current_sequence = ""
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
self.logger.error(f"Error in _init_grpGeneViewer: {str(e)}")
|
| 138 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 139 |
|
| 140 |
def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
|
| 141 |
widget = self.findChild(widget_type, name)
|
|
|
|
| 143 |
self.logger.warning(f"Widget '{name}' not found in UI file.")
|
| 144 |
return widget
|
| 145 |
|
| 146 |
+
def display_guides_in_table(self, guides, suppress_updates=False):
|
| 147 |
try:
|
| 148 |
self._all_guides = guides
|
|
|
|
| 149 |
selected_text = self.combo_box_gene.currentText()
|
| 150 |
|
| 151 |
# First filter by position/feature
|
|
|
|
| 166 |
filtered_guides.append(guide)
|
| 167 |
else:
|
| 168 |
filtered_guides = self._all_guides
|
| 169 |
+
self.logger.debug("No filtering applied, using all guides")
|
| 170 |
|
| 171 |
# Apply additional filters
|
| 172 |
final_guides = []
|
|
|
|
| 186 |
|
| 187 |
# Update table with new guides
|
| 188 |
total_rows = len(final_guides)
|
|
|
|
| 189 |
|
| 190 |
+
# Completely freeze UI only if not suppressing updates
|
| 191 |
+
if not suppress_updates:
|
| 192 |
+
self.setUpdatesEnabled(False)
|
| 193 |
+
self.table_guides.setUpdatesEnabled(False)
|
| 194 |
+
self.table_guides.setSortingEnabled(False)
|
| 195 |
+
self.table_guides.setVisible(False)
|
| 196 |
|
| 197 |
try:
|
| 198 |
# Clear and resize table
|
|
|
|
| 266 |
guide_viewer_group.setMinimumWidth(essential_columns_width + 50) # Add some padding for scrollbar
|
| 267 |
|
| 268 |
finally:
|
| 269 |
+
# Re-enable UI only if not suppressing updates
|
| 270 |
+
if not suppress_updates:
|
| 271 |
+
self.table_guides.setVisible(True)
|
| 272 |
+
self.table_guides.setUpdatesEnabled(True)
|
| 273 |
+
self.setUpdatesEnabled(True)
|
| 274 |
+
self.table_guides.setSortingEnabled(True)
|
| 275 |
|
| 276 |
except Exception as e:
|
| 277 |
self.logger.error(f"Error in display_guides: {str(e)}")
|
|
|
|
| 420 |
# Clear existing items efficiently
|
| 421 |
self.combo_box_gene.clear()
|
| 422 |
|
|
|
|
|
|
|
|
|
|
| 423 |
# Use a set to ensure uniqueness
|
| 424 |
unique_genes = list(set(genes))
|
| 425 |
|
|
|
|
| 431 |
# Set first item without triggering updates
|
| 432 |
if self.combo_box_gene.count() > 0:
|
| 433 |
self.combo_box_gene.setCurrentIndex(0)
|
|
|
|
|
|
|
| 434 |
|
| 435 |
# Re-enable UI updates
|
| 436 |
self.combo_box_gene.setUpdatesEnabled(True)
|
|
|
|
| 454 |
|
| 455 |
def update_gene_viewer(self, sequence, features=None):
|
| 456 |
"""Update both text editor and DNA feature viewer"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
try:
|
| 458 |
+
# Update text editor
|
| 459 |
+
self.text_edit_gene_viewer.clear()
|
| 460 |
+
doc = QTextDocument()
|
| 461 |
+
doc.setHtml(sequence)
|
| 462 |
+
self.text_edit_gene_viewer.setDocument(doc)
|
| 463 |
+
|
| 464 |
+
# Get start position from line edit
|
| 465 |
+
try:
|
| 466 |
+
start_pos = int(self.line_edit_start_location.text())
|
| 467 |
+
except (ValueError, TypeError):
|
| 468 |
+
start_pos = 1
|
| 469 |
+
|
| 470 |
+
# Update DNA feature viewer
|
| 471 |
+
if features is None:
|
| 472 |
+
features = []
|
| 473 |
+
self.dna_feature_viewer.set_data(sequence, features, start_pos)
|
| 474 |
+
|
| 475 |
+
except Exception as e:
|
| 476 |
+
self.logger.error(f"Error updating gene viewer: {str(e)}")
|
| 477 |
|
| 478 |
def select_all_guides(self, select):
|
| 479 |
for row in range(self.table_guides.rowCount()):
|
|
|
|
| 588 |
self.logger.error(f"Error showing details: {str(e)}")
|
| 589 |
show_error(self.settings, "Error showing details", str(e))
|
| 590 |
|
| 591 |
+
def clear_highlights(self):
|
| 592 |
+
"""Clear highlights in viewer"""
|
| 593 |
+
self.dna_feature_viewer.sequence_viewer.clear_highlights()
|
|
|
|
| 594 |
|
| 595 |
+
def eventFilter(self, obj, event):
|
| 596 |
+
"""Handle key press events"""
|
| 597 |
+
if event.type() == QEvent.Type.KeyPress:
|
| 598 |
+
# Check for copy command
|
| 599 |
+
if event.matches(QKeySequence.StandardKey.Copy):
|
| 600 |
+
self._handle_copy()
|
| 601 |
+
return True
|
| 602 |
+
|
| 603 |
+
# Handle delete/backspace
|
| 604 |
+
if event.key() in [Qt.Key.Key_Delete, Qt.Key.Key_Backspace]:
|
| 605 |
+
return self._handle_delete()
|
| 606 |
+
|
| 607 |
+
# Handle only valid base pair keys
|
| 608 |
+
if event.text():
|
| 609 |
+
valid_bases = set('ATGCRYMKSWBDHVNatgcrymkswbdhvn')
|
| 610 |
+
if event.text() in valid_bases:
|
| 611 |
+
return self._handle_insert()
|
| 612 |
+
|
| 613 |
+
return super().eventFilter(obj, event)
|
| 614 |
+
|
| 615 |
+
def _handle_copy(self):
|
| 616 |
+
"""Copy selected sequence to clipboard"""
|
| 617 |
try:
|
| 618 |
+
start = self.dna_feature_viewer.insertion_zone.selection_start
|
| 619 |
+
end = self.dna_feature_viewer.insertion_zone.current_cursor_pos
|
| 620 |
+
|
| 621 |
+
if start is not None and end is not None:
|
| 622 |
+
start, end = min(start, end), max(start, end)
|
| 623 |
|
| 624 |
+
# Get sequence from DNA viewer instead of stored sequence
|
| 625 |
+
sequence = self.dna_feature_viewer.sequence_viewer.sequence
|
| 626 |
+
if sequence:
|
| 627 |
+
selected_sequence = sequence[start:end]
|
| 628 |
+
if selected_sequence:
|
| 629 |
+
clipboard = QApplication.clipboard()
|
| 630 |
+
clipboard.setText(selected_sequence)
|
| 631 |
+
self.logger.debug(f"Copied sequence: {selected_sequence}")
|
| 632 |
+
else:
|
| 633 |
+
self.logger.warning("No sequence available to copy")
|
| 634 |
+
|
| 635 |
+
except Exception as e:
|
| 636 |
+
self.logger.error(f"Error copying sequence: {str(e)}")
|
| 637 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 638 |
+
|
| 639 |
+
def _handle_delete(self):
|
| 640 |
+
"""Handle deletion of selected base pairs"""
|
| 641 |
+
start = self.dna_feature_viewer.insertion_zone.selection_start
|
| 642 |
+
end = self.dna_feature_viewer.insertion_zone.current_cursor_pos
|
| 643 |
+
|
| 644 |
+
# Check if there's no selection but there are highlighted nucleotides
|
| 645 |
+
if (start is None or end is None or start == end):
|
| 646 |
+
# Look for highlighted nucleotides
|
| 647 |
+
sequence_viewer = self.dna_feature_viewer.sequence_viewer
|
| 648 |
+
highlighted_positions = []
|
| 649 |
+
|
| 650 |
+
for i, nuc in enumerate(sequence_viewer.nucleotides):
|
| 651 |
+
if nuc.is_highlighted and nuc.highlight_color == QColor(100, 150, 255, 100): # Check for selection blue
|
| 652 |
+
# Convert nucleotide index to sequence position (divide by 2 since each base has 2 nucleotides)
|
| 653 |
+
pos = i // 2
|
| 654 |
+
highlighted_positions.append(pos)
|
| 655 |
+
|
| 656 |
+
if highlighted_positions:
|
| 657 |
+
# Use the range of highlighted positions
|
| 658 |
+
start = min(highlighted_positions)
|
| 659 |
+
end = max(highlighted_positions) + 1 # Add 1 to include the last position
|
| 660 |
+
else:
|
| 661 |
+
QMessageBox.warning(
|
| 662 |
+
self,
|
| 663 |
+
"No Selection",
|
| 664 |
+
"Please select the bases to be removed and then press Delete",
|
| 665 |
+
QMessageBox.StandardButton.Ok
|
| 666 |
+
)
|
| 667 |
+
return True
|
| 668 |
+
|
| 669 |
+
start, end = min(start, end), max(start, end)
|
| 670 |
+
num_bases = end - start
|
| 671 |
+
|
| 672 |
+
reply = QMessageBox.question(
|
| 673 |
+
self,
|
| 674 |
+
"Confirm Deletion",
|
| 675 |
+
f"Delete {num_bases} bp?",
|
| 676 |
+
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
|
| 677 |
+
QMessageBox.StandardButton.Cancel
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
if reply == QMessageBox.StandardButton.Ok:
|
| 681 |
+
# Store current highlights before deletion
|
| 682 |
+
guide_highlights = []
|
| 683 |
+
sequence_viewer = self.dna_feature_viewer.sequence_viewer
|
| 684 |
+
for i, nuc in enumerate(sequence_viewer.nucleotides):
|
| 685 |
+
if nuc.is_highlighted:
|
| 686 |
+
# Only store guide highlights (red/green), not selection highlights (blue)
|
| 687 |
+
if nuc.highlight_color not in [QColor(200, 200, 255, 100), QColor(100, 150, 255, 100)]:
|
| 688 |
+
# Store position and color
|
| 689 |
+
guide_highlights.append({
|
| 690 |
+
'pos': i,
|
| 691 |
+
'color': nuc.highlight_color,
|
| 692 |
+
'strand': '-' if i % 2 else '+' # Odd indices are negative strand
|
| 693 |
+
})
|
| 694 |
+
|
| 695 |
+
# Create new sequence
|
| 696 |
+
new_sequence = self._current_sequence[:start] + self._current_sequence[end:]
|
| 697 |
+
self._current_sequence = new_sequence
|
| 698 |
+
|
| 699 |
+
# Clear all highlights before updating viewer
|
| 700 |
+
self.dna_feature_viewer.sequence_viewer.clear_highlights()
|
| 701 |
+
|
| 702 |
+
# Update viewer with new sequence
|
| 703 |
+
self.update_gene_viewer(new_sequence)
|
| 704 |
+
|
| 705 |
+
# Reapply guide highlights, adjusting positions for deleted section
|
| 706 |
+
for highlight in guide_highlights:
|
| 707 |
+
orig_pos = highlight['pos']
|
| 708 |
+
# Calculate new position after deletion
|
| 709 |
+
if orig_pos < start * 2: # Multiply by 2 because each base has two nucleotides
|
| 710 |
+
new_pos = orig_pos
|
| 711 |
+
elif orig_pos > end * 2:
|
| 712 |
+
new_pos = orig_pos - ((end - start) * 2) # Adjust for deleted section
|
| 713 |
+
else:
|
| 714 |
+
continue # Skip highlights in deleted region
|
| 715 |
+
|
| 716 |
+
# Apply highlight to new position
|
| 717 |
+
if new_pos < len(sequence_viewer.nucleotides):
|
| 718 |
+
nuc = sequence_viewer.nucleotides[new_pos]
|
| 719 |
+
nuc.is_highlighted = True
|
| 720 |
+
nuc.highlight_color = highlight['color']
|
| 721 |
+
nuc.update()
|
| 722 |
+
|
| 723 |
+
# Calculate cursor position coordinates
|
| 724 |
+
line_number = start // self.dna_feature_viewer.sequence_viewer.bases_per_line
|
| 725 |
+
cursor_pos_in_line = start % self.dna_feature_viewer.sequence_viewer.bases_per_line
|
| 726 |
+
|
| 727 |
+
# Calculate exact cursor coordinates
|
| 728 |
+
cursor_x = self.dna_feature_viewer.sequence_viewer.strand_margin + (cursor_pos_in_line * self.dna_feature_viewer.sequence_viewer.base_width)
|
| 729 |
+
cursor_y = (line_number * self.dna_feature_viewer.sequence_viewer.line_spacing) + (self.dna_feature_viewer.sequence_viewer.line_height * 0.1)
|
| 730 |
+
cursor_height = self.dna_feature_viewer.sequence_viewer.line_height * 2 + 6
|
| 731 |
+
|
| 732 |
+
# Position cursor at start of deleted region
|
| 733 |
+
self.dna_feature_viewer.insertion_zone.sequence_cursor.set_position(
|
| 734 |
+
cursor_x,
|
| 735 |
+
cursor_y,
|
| 736 |
+
cursor_height
|
| 737 |
+
)
|
| 738 |
+
self.dna_feature_viewer.insertion_zone.sequence_cursor.show()
|
| 739 |
+
|
| 740 |
+
# Update stored cursor position
|
| 741 |
+
self.dna_feature_viewer.insertion_zone.current_cursor_pos = start
|
| 742 |
+
|
| 743 |
+
# Clear selection states
|
| 744 |
+
self.dna_feature_viewer.sequence_viewer.selection_start = None
|
| 745 |
+
self.dna_feature_viewer.sequence_viewer.selection_end = None
|
| 746 |
+
self.dna_feature_viewer.sequence_viewer.selection_active = False
|
| 747 |
+
self.dna_feature_viewer.insertion_zone.selection_start = None
|
| 748 |
+
self.dna_feature_viewer.insertion_zone.selection_end = None
|
| 749 |
+
self.dna_feature_viewer.insertion_zone.selection_active = False
|
| 750 |
+
|
| 751 |
+
# Force update of the view
|
| 752 |
+
scene = self.dna_feature_viewer.scene
|
| 753 |
+
if scene is not None:
|
| 754 |
+
scene.update()
|
| 755 |
+
|
| 756 |
+
# Make sure view maintains focus
|
| 757 |
+
self.dna_feature_viewer.view.setFocus()
|
| 758 |
+
|
| 759 |
+
return True
|
| 760 |
+
|
| 761 |
+
def _handle_insert(self):
|
| 762 |
+
"""Handle base pair insertion"""
|
| 763 |
+
cursor_pos = self.dna_feature_viewer.insertion_zone.current_cursor_pos
|
| 764 |
+
if cursor_pos is not None:
|
| 765 |
+
dialog = BaseInsertionDialog(self)
|
| 766 |
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
| 767 |
+
bases = dialog.get_bases()
|
| 768 |
|
| 769 |
+
new_sequence = (
|
| 770 |
+
self._current_sequence[:cursor_pos] +
|
| 771 |
+
bases +
|
| 772 |
+
self._current_sequence[cursor_pos:]
|
| 773 |
+
)
|
| 774 |
+
self._current_sequence = new_sequence
|
| 775 |
|
| 776 |
+
# Store new cursor position before updating viewer
|
| 777 |
+
new_cursor_pos = cursor_pos + len(bases)
|
| 778 |
+
|
| 779 |
+
# Update viewer with new sequence
|
| 780 |
+
self.update_gene_viewer(new_sequence)
|
| 781 |
+
|
| 782 |
+
# Highlight the newly added bases on both strands
|
| 783 |
+
highlight_color = QColor(100, 150, 255, 100) # Same blue as selection
|
| 784 |
+
# Highlight positive strand
|
| 785 |
+
self.dna_feature_viewer.sequence_viewer.highlight_sequence(
|
| 786 |
+
cursor_pos, # Start at insertion point
|
| 787 |
+
new_cursor_pos - 1, # End at position before new cursor
|
| 788 |
+
highlight_color,
|
| 789 |
+
strand='+'
|
| 790 |
+
)
|
| 791 |
+
# Highlight negative strand
|
| 792 |
+
self.dna_feature_viewer.sequence_viewer.highlight_sequence(
|
| 793 |
+
cursor_pos, # Start at insertion point
|
| 794 |
+
new_cursor_pos - 1, # End at position before new cursor
|
| 795 |
+
highlight_color,
|
| 796 |
+
strand='-'
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
# Calculate cursor position coordinates
|
| 800 |
+
line_number = new_cursor_pos // self.dna_feature_viewer.sequence_viewer.bases_per_line
|
| 801 |
+
cursor_pos_in_line = new_cursor_pos % self.dna_feature_viewer.sequence_viewer.bases_per_line
|
| 802 |
+
|
| 803 |
+
# Calculate exact cursor coordinates
|
| 804 |
+
cursor_x = self.dna_feature_viewer.sequence_viewer.strand_margin + (cursor_pos_in_line * self.dna_feature_viewer.sequence_viewer.base_width)
|
| 805 |
+
cursor_y = (line_number * self.dna_feature_viewer.sequence_viewer.line_spacing) + (self.dna_feature_viewer.sequence_viewer.line_height * 0.1)
|
| 806 |
+
cursor_height = self.dna_feature_viewer.sequence_viewer.line_height * 2 + 6
|
| 807 |
+
|
| 808 |
+
# Position cursor after inserted bases
|
| 809 |
+
self.dna_feature_viewer.insertion_zone.sequence_cursor.set_position(
|
| 810 |
+
cursor_x,
|
| 811 |
+
cursor_y,
|
| 812 |
+
cursor_height
|
| 813 |
+
)
|
| 814 |
+
self.dna_feature_viewer.insertion_zone.sequence_cursor.show()
|
| 815 |
+
|
| 816 |
+
# Update stored cursor position
|
| 817 |
+
self.dna_feature_viewer.insertion_zone.current_cursor_pos = new_cursor_pos
|
| 818 |
+
|
| 819 |
+
# Make sure view maintains focus
|
| 820 |
+
self.dna_feature_viewer.view.setFocus()
|
| 821 |
+
|
| 822 |
+
return True
|
| 823 |
+
|
| 824 |
+
def closeEvent(self, event):
|
| 825 |
+
"""Handle cleanup when window is closed"""
|
| 826 |
+
try:
|
| 827 |
+
# Clean up DNA viewer if it exists
|
| 828 |
+
if hasattr(self, 'dna_feature_viewer'):
|
| 829 |
+
self.dna_feature_viewer.closeEvent(event)
|
| 830 |
+
|
| 831 |
+
# Ensure all views release mouse tracking
|
| 832 |
+
for child in self.findChildren(QtWidgets.QWidget):
|
| 833 |
+
if isinstance(child, (QtWidgets.QGraphicsView, QtWidgets.QAbstractScrollArea)):
|
| 834 |
+
child.setMouseTracking(False)
|
| 835 |
+
child.viewport().setMouseTracking(False)
|
| 836 |
+
if child.hasMouseTracking():
|
| 837 |
+
child.releaseMouse()
|
| 838 |
|
| 839 |
except Exception as e:
|
| 840 |
+
if hasattr(self, 'logger'):
|
| 841 |
+
self.logger.error(f"Error in closeEvent: {str(e)}")
|
| 842 |
+
|
| 843 |
+
super().closeEvent(event)
|
|
|
|
|
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton
|
| 2 |
+
|
| 3 |
+
class BaseInsertionDialog(QDialog):
|
| 4 |
+
"""Dialog for inserting base pairs"""
|
| 5 |
+
def __init__(self, parent=None):
|
| 6 |
+
super().__init__(parent)
|
| 7 |
+
self.setWindowTitle("Insert Base Pairs")
|
| 8 |
+
self.setModal(True)
|
| 9 |
+
|
| 10 |
+
self.valid_bases = set('ATGCRYMKSWBDHVNatgcrymkswbdhvn')
|
| 11 |
+
|
| 12 |
+
layout = QVBoxLayout(self)
|
| 13 |
+
|
| 14 |
+
label = QLabel(
|
| 15 |
+
"Enter base pairs to insert:\n"
|
| 16 |
+
"A (Adenine), T (Thymine), G (Guanine), C (Cytosine)\n"
|
| 17 |
+
"R (A/G), Y (C/T), M (A/C), K (G/T), S (G/C), W (A/T)\n"
|
| 18 |
+
"H (A/C/T), B (G/C/T), V (G/C/A), D (G/A/T), N (Any)"
|
| 19 |
+
)
|
| 20 |
+
layout.addWidget(label)
|
| 21 |
+
|
| 22 |
+
self.input_field = QLineEdit()
|
| 23 |
+
self.input_field.setPlaceholderText("e.g., ATGC, RYKMSWBDHVN")
|
| 24 |
+
self.input_field.textChanged.connect(self._validate_input)
|
| 25 |
+
layout.addWidget(self.input_field)
|
| 26 |
+
|
| 27 |
+
button_layout = QHBoxLayout()
|
| 28 |
+
|
| 29 |
+
self.insert_button = QPushButton("Insert")
|
| 30 |
+
self.insert_button.clicked.connect(self.accept)
|
| 31 |
+
self.insert_button.setEnabled(False) # Disabled until valid input
|
| 32 |
+
|
| 33 |
+
cancel_button = QPushButton("Cancel")
|
| 34 |
+
cancel_button.clicked.connect(self.reject)
|
| 35 |
+
|
| 36 |
+
button_layout.addWidget(self.insert_button)
|
| 37 |
+
button_layout.addWidget(cancel_button)
|
| 38 |
+
layout.addLayout(button_layout)
|
| 39 |
+
|
| 40 |
+
self.setMinimumWidth(400)
|
| 41 |
+
|
| 42 |
+
def _validate_input(self, text):
|
| 43 |
+
"""Validate input and filter invalid characters"""
|
| 44 |
+
# Remove any invalid characters
|
| 45 |
+
valid_text = ''.join(c for c in text if c in self.valid_bases)
|
| 46 |
+
|
| 47 |
+
# If text changed, update the field
|
| 48 |
+
if valid_text != text:
|
| 49 |
+
self.input_field.setText(valid_text)
|
| 50 |
+
|
| 51 |
+
# Enable/disable insert button based on input
|
| 52 |
+
self.insert_button.setEnabled(bool(valid_text))
|
| 53 |
+
|
| 54 |
+
def get_bases(self):
|
| 55 |
+
"""Return the entered base pairs preserving case"""
|
| 56 |
+
return self.input_field.text()
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to make the directory a Python package
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to make the directory a Python package
|
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import QGraphicsObject
|
| 2 |
+
from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QPointF
|
| 3 |
+
from PyQt6.QtGui import QBrush, QColor, QFont
|
| 4 |
+
|
| 5 |
+
class FeatureViewer(QGraphicsObject):
|
| 6 |
+
"""Component for displaying DNA features like genes and exons"""
|
| 7 |
+
cursor_position_changed = pyqtSignal(int)
|
| 8 |
+
|
| 9 |
+
def __init__(self, parent=None):
|
| 10 |
+
super().__init__(parent)
|
| 11 |
+
self.sequence = ""
|
| 12 |
+
self.features = []
|
| 13 |
+
self.start_pos = 0
|
| 14 |
+
self.base_width = 15
|
| 15 |
+
self.bases_per_line = 70
|
| 16 |
+
self.feature_height = 20
|
| 17 |
+
self.line_height = 25
|
| 18 |
+
self.feature_spacing = 2 # Spacing between features and strands
|
| 19 |
+
self.setAcceptHoverEvents(True)
|
| 20 |
+
|
| 21 |
+
# Add logger
|
| 22 |
+
import logging
|
| 23 |
+
self.logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
def set_data(self, sequence, features, start_pos):
|
| 26 |
+
"""Set sequence and features data"""
|
| 27 |
+
self.sequence = sequence
|
| 28 |
+
self.features = sorted(features, key=lambda x: x['start'])
|
| 29 |
+
self.start_pos = start_pos
|
| 30 |
+
self.update()
|
| 31 |
+
|
| 32 |
+
def paint(self, painter, option, widget):
|
| 33 |
+
"""Paint the features"""
|
| 34 |
+
if not self.features or not self.sequence:
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
# Process each line of sequence
|
| 38 |
+
current_pos = 0
|
| 39 |
+
while current_pos < len(self.sequence):
|
| 40 |
+
line_text = self.sequence[current_pos:current_pos + self.bases_per_line]
|
| 41 |
+
line_num = current_pos // self.bases_per_line
|
| 42 |
+
|
| 43 |
+
# Calculate y position to be directly below negative strand
|
| 44 |
+
y_pos = line_num * self.line_height * 2
|
| 45 |
+
feature_y = y_pos + self.line_height * 2 # Position below negative strand
|
| 46 |
+
|
| 47 |
+
# Calculate sequence width for this line
|
| 48 |
+
sequence_width = len(line_text) * self.base_width
|
| 49 |
+
|
| 50 |
+
# Draw features
|
| 51 |
+
for feature in self.features:
|
| 52 |
+
try:
|
| 53 |
+
# Calculate relative positions within current line
|
| 54 |
+
feature_start = feature['start'] - current_pos
|
| 55 |
+
feature_end = feature['end'] - current_pos
|
| 56 |
+
|
| 57 |
+
# Skip if feature is not in current line
|
| 58 |
+
if feature_end < 0 or feature_start >= self.bases_per_line:
|
| 59 |
+
continue
|
| 60 |
+
|
| 61 |
+
# Clip to line boundaries
|
| 62 |
+
feature_start = max(0, feature_start)
|
| 63 |
+
feature_end = min(self.bases_per_line, feature_end)
|
| 64 |
+
|
| 65 |
+
# Calculate pixel positions
|
| 66 |
+
x_start = feature_start * self.base_width
|
| 67 |
+
x_end = feature_end * self.base_width
|
| 68 |
+
|
| 69 |
+
# Create rectangle for feature
|
| 70 |
+
feature_rect = QRectF(
|
| 71 |
+
x_start,
|
| 72 |
+
feature_y,
|
| 73 |
+
x_end - x_start,
|
| 74 |
+
self.feature_height
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Set color based on feature type
|
| 78 |
+
if feature.get('type') == 'exon':
|
| 79 |
+
color = QColor(100, 180, 255) # Light blue for exons
|
| 80 |
+
else:
|
| 81 |
+
color = QColor(255, 140, 0) # Orange for genes
|
| 82 |
+
|
| 83 |
+
# Draw feature rectangle
|
| 84 |
+
painter.setBrush(QBrush(color))
|
| 85 |
+
painter.setPen(Qt.PenStyle.NoPen)
|
| 86 |
+
painter.drawRect(feature_rect)
|
| 87 |
+
|
| 88 |
+
# Draw label if enough space
|
| 89 |
+
label = feature.get('name', '')
|
| 90 |
+
text_width = painter.fontMetrics().horizontalAdvance(label)
|
| 91 |
+
if (x_end - x_start) > text_width:
|
| 92 |
+
text_x = x_start + ((x_end - x_start) - text_width) / 2
|
| 93 |
+
text_y = feature_y + self.feature_height/2 + 4
|
| 94 |
+
painter.setPen(Qt.GlobalColor.white)
|
| 95 |
+
painter.setFont(QFont("Arial", 8))
|
| 96 |
+
painter.drawText(QPointF(text_x, text_y), label)
|
| 97 |
+
|
| 98 |
+
except Exception as e:
|
| 99 |
+
self.logger.error(f"Error drawing feature: {str(e)}")
|
| 100 |
+
continue
|
| 101 |
+
|
| 102 |
+
current_pos += self.bases_per_line
|
| 103 |
+
|
| 104 |
+
def boundingRect(self):
|
| 105 |
+
"""Return the bounding rectangle of the component"""
|
| 106 |
+
if not self.sequence:
|
| 107 |
+
return QRectF()
|
| 108 |
+
|
| 109 |
+
# Calculate exact width based on sequence length
|
| 110 |
+
last_line_length = len(self.sequence) % self.bases_per_line
|
| 111 |
+
if last_line_length == 0:
|
| 112 |
+
last_line_length = self.bases_per_line
|
| 113 |
+
width = max(self.base_width * self.bases_per_line,
|
| 114 |
+
self.base_width * last_line_length) + 100
|
| 115 |
+
|
| 116 |
+
# Calculate height for actual sequence lines
|
| 117 |
+
total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
|
| 118 |
+
height = total_lines * self.line_height * 2
|
| 119 |
+
|
| 120 |
+
return QRectF(0, 0, width, height)
|
| 121 |
+
|
| 122 |
+
def mousePressEvent(self, event):
|
| 123 |
+
"""Handle mouse press to show insertion point"""
|
| 124 |
+
if event.button() == Qt.MouseButton.LeftButton:
|
| 125 |
+
# Calculate position based on click location
|
| 126 |
+
local_pos = event.pos()
|
| 127 |
+
line_number = int(local_pos.y() // (self.line_height * 2))
|
| 128 |
+
base_position = int(local_pos.x() // self.base_width)
|
| 129 |
+
|
| 130 |
+
# Calculate absolute position
|
| 131 |
+
position = self.start_pos + line_number * self.bases_per_line + base_position
|
| 132 |
+
|
| 133 |
+
# Emit cursor position
|
| 134 |
+
self.cursor_position_changed.emit(position)
|
| 135 |
+
|
| 136 |
+
event.accept()
|
| 137 |
+
|
| 138 |
+
def mouseMoveEvent(self, event):
|
| 139 |
+
"""Handle mouse move to update insertion point"""
|
| 140 |
+
if event.buttons() & Qt.MouseButton.LeftButton:
|
| 141 |
+
# Calculate position based on mouse location
|
| 142 |
+
local_pos = event.pos()
|
| 143 |
+
line_number = int(local_pos.y() // (self.line_height * 2))
|
| 144 |
+
base_position = int(local_pos.x() // self.base_width)
|
| 145 |
+
|
| 146 |
+
# Calculate absolute position
|
| 147 |
+
position = self.start_pos + line_number * self.bases_per_line + base_position
|
| 148 |
+
|
| 149 |
+
# Emit cursor position
|
| 150 |
+
self.cursor_position_changed.emit(position)
|
| 151 |
+
|
| 152 |
+
event.accept()
|
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import QGraphicsObject
|
| 2 |
+
from PyQt6.QtCore import Qt, QRectF, pyqtSignal
|
| 3 |
+
from PyQt6.QtGui import QFont, QColor
|
| 4 |
+
|
| 5 |
+
class NucleotideItem(QGraphicsObject):
|
| 6 |
+
"""Component for displaying individual nucleotides in the gene sequence viewer"""
|
| 7 |
+
clicked = pyqtSignal(object)
|
| 8 |
+
|
| 9 |
+
def __init__(self, nucleotide, x, y, width, is_uppercase=True, is_complement=False, is_padding=False, parent=None):
|
| 10 |
+
"""Initialize nucleotide item
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
nucleotide (str): The nucleotide base (A, T, G, C)
|
| 14 |
+
x (float): X position
|
| 15 |
+
y (float): Y position
|
| 16 |
+
width (float): Width of nucleotide
|
| 17 |
+
is_uppercase (bool): Whether nucleotide should be uppercase
|
| 18 |
+
is_complement (bool): Whether this is a complement strand nucleotide
|
| 19 |
+
is_padding (bool): Whether this nucleotide is part of padding sequence
|
| 20 |
+
parent (QGraphicsObject): Parent item
|
| 21 |
+
"""
|
| 22 |
+
super().__init__(parent)
|
| 23 |
+
self.nucleotide = nucleotide
|
| 24 |
+
self.setPos(x, y)
|
| 25 |
+
self.width = width
|
| 26 |
+
self.base_width = width
|
| 27 |
+
self.height = 20
|
| 28 |
+
self.is_uppercase = is_uppercase
|
| 29 |
+
self.is_complement = is_complement
|
| 30 |
+
self.is_padding = is_padding
|
| 31 |
+
self.is_highlighted = False
|
| 32 |
+
self.highlight_color = None
|
| 33 |
+
|
| 34 |
+
# Enable hover events
|
| 35 |
+
self.setAcceptHoverEvents(True)
|
| 36 |
+
|
| 37 |
+
def paint(self, painter, option, widget):
|
| 38 |
+
"""Paint the nucleotide"""
|
| 39 |
+
try:
|
| 40 |
+
# Draw highlight background if highlighted
|
| 41 |
+
if self.is_highlighted and self.highlight_color:
|
| 42 |
+
painter.save()
|
| 43 |
+
painter.fillRect(self.boundingRect(), self.highlight_color)
|
| 44 |
+
painter.restore()
|
| 45 |
+
|
| 46 |
+
# Draw nucleotide centered
|
| 47 |
+
painter.setFont(QFont("Courier", 12))
|
| 48 |
+
|
| 49 |
+
# Use grey for padding sequence, black for all other nucleotides
|
| 50 |
+
if self.is_padding:
|
| 51 |
+
painter.setPen(QColor(100, 100, 100))
|
| 52 |
+
else:
|
| 53 |
+
painter.setPen(Qt.GlobalColor.black)
|
| 54 |
+
|
| 55 |
+
# Get complement nucleotide if needed
|
| 56 |
+
display_nucleotide = self._get_complement() if self.is_complement else self.nucleotide
|
| 57 |
+
|
| 58 |
+
# Draw text centered
|
| 59 |
+
painter.drawText(self.boundingRect(), Qt.AlignmentFlag.AlignCenter, display_nucleotide)
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"Error in paint: {str(e)}")
|
| 63 |
+
|
| 64 |
+
def boundingRect(self):
|
| 65 |
+
"""Return the bounding rectangle for the nucleotide"""
|
| 66 |
+
return QRectF(0, 0, self.width, self.height)
|
| 67 |
+
|
| 68 |
+
def mousePressEvent(self, event):
|
| 69 |
+
"""Handle mouse press events"""
|
| 70 |
+
if event.button() == Qt.MouseButton.LeftButton:
|
| 71 |
+
sequence_viewer = self.parent()
|
| 72 |
+
if sequence_viewer:
|
| 73 |
+
pos = sequence_viewer.get_nucleotide_position(self)
|
| 74 |
+
local_x = event.pos().x()
|
| 75 |
+
|
| 76 |
+
# Calculate position relative to letter boundaries
|
| 77 |
+
text_x = (self.width - self.base_width) / 2
|
| 78 |
+
relative_x = local_x - text_x
|
| 79 |
+
|
| 80 |
+
# Determine cursor position
|
| 81 |
+
if relative_x <= 0: # Before letter
|
| 82 |
+
cursor_pos = pos
|
| 83 |
+
elif relative_x >= self.base_width: # After letter
|
| 84 |
+
cursor_pos = pos + 1
|
| 85 |
+
else: # On letter
|
| 86 |
+
cursor_pos = pos + (1 if relative_x > self.base_width / 2 else 0)
|
| 87 |
+
|
| 88 |
+
# Update selection and cursor
|
| 89 |
+
sequence_viewer.selection_start = cursor_pos
|
| 90 |
+
sequence_viewer.selection_end = cursor_pos
|
| 91 |
+
sequence_viewer.selection_active = True
|
| 92 |
+
|
| 93 |
+
sequence_viewer.cursor_position_changed.emit(cursor_pos)
|
| 94 |
+
sequence_viewer._update_selection()
|
| 95 |
+
self.update()
|
| 96 |
+
|
| 97 |
+
event.accept()
|
| 98 |
+
|
| 99 |
+
def _get_complement(self):
|
| 100 |
+
"""Get complement nucleotide"""
|
| 101 |
+
complement_map = {
|
| 102 |
+
'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G',
|
| 103 |
+
'a': 't', 't': 'a', 'g': 'c', 'c': 'g',
|
| 104 |
+
'K': 'M', 'M': 'K', 'R': 'Y', 'Y': 'R',
|
| 105 |
+
'k': 'm', 'm': 'k', 'r': 'y', 'y': 'r',
|
| 106 |
+
'S': 'S', 's': 's'
|
| 107 |
+
}
|
| 108 |
+
return complement_map.get(self.nucleotide, self.nucleotide)
|
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from PyQt6.QtWidgets import QGraphicsScene, QGraphicsLineItem, QGraphicsSimpleTextItem
|
| 3 |
+
from PyQt6.QtCore import QRectF, Qt
|
| 4 |
+
from PyQt6.QtGui import QPen, QColor, QFont, QBrush
|
| 5 |
+
|
| 6 |
+
class Ruler(QGraphicsScene):
|
| 7 |
+
"""Component for displaying the ruler with position markers"""
|
| 8 |
+
|
| 9 |
+
def __init__(self, parent=None):
|
| 10 |
+
super().__init__(parent)
|
| 11 |
+
self.logger = logging.getLogger(__name__)
|
| 12 |
+
self.base_width = 15
|
| 13 |
+
self.strand_margin = 40 # Width for 5' and 3' indicators
|
| 14 |
+
self.ruler_height = 30
|
| 15 |
+
self.ruler_color = QColor(0, 120, 215) # Blue
|
| 16 |
+
|
| 17 |
+
# Set background color to match interface
|
| 18 |
+
background_color = QColor(240, 240, 240)
|
| 19 |
+
self.setBackgroundBrush(QBrush(background_color))
|
| 20 |
+
|
| 21 |
+
# Ensure the background fills the entire scene
|
| 22 |
+
self.setSceneRect(QRectF(0, 0, 1000, self.ruler_height - 15))
|
| 23 |
+
|
| 24 |
+
# Tick mark settings
|
| 25 |
+
self.major_tick_height = 10 # Height for major ticks (multiples of 10)
|
| 26 |
+
self.medium_tick_height = 7 # Height for medium ticks (multiples of 5)
|
| 27 |
+
self.minor_tick_height = 4 # Height for minor ticks
|
| 28 |
+
|
| 29 |
+
self.ruler_y = 15 # Y position of main ruler line
|
| 30 |
+
|
| 31 |
+
# Initialize tracker line as None - we'll create it when needed
|
| 32 |
+
self.tracker_line = None
|
| 33 |
+
|
| 34 |
+
def create_ruler(self, bases_per_line):
|
| 35 |
+
"""Create ruler with position markers
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
bases_per_line (int): Number of bases per line in sequence viewer
|
| 39 |
+
"""
|
| 40 |
+
try:
|
| 41 |
+
self.clear() # Clear everything
|
| 42 |
+
|
| 43 |
+
# Create horizontal blue line aligned with sequence
|
| 44 |
+
ruler_line = QGraphicsLineItem(
|
| 45 |
+
self.strand_margin, self.ruler_y,
|
| 46 |
+
bases_per_line * self.base_width + self.strand_margin, self.ruler_y
|
| 47 |
+
)
|
| 48 |
+
ruler_line.setPen(QPen(self.ruler_color, 1))
|
| 49 |
+
self.addItem(ruler_line)
|
| 50 |
+
|
| 51 |
+
# Add tick marks and numbers
|
| 52 |
+
for i in range(0, bases_per_line):
|
| 53 |
+
x_pos = i * self.base_width + self.strand_margin + self.base_width/2
|
| 54 |
+
pos_1_based = i + 1
|
| 55 |
+
|
| 56 |
+
# Determine tick properties based on position
|
| 57 |
+
if pos_1_based % 10 == 0: # Major ticks (every 10)
|
| 58 |
+
tick_height = self.major_tick_height
|
| 59 |
+
tick_start = 10
|
| 60 |
+
# Add number
|
| 61 |
+
text = QGraphicsSimpleTextItem(str(pos_1_based))
|
| 62 |
+
text.setFont(QFont("Arial", 8))
|
| 63 |
+
text_width = text.boundingRect().width()
|
| 64 |
+
text.setPos(x_pos - text_width/2, 0)
|
| 65 |
+
self.addItem(text)
|
| 66 |
+
elif pos_1_based % 5 == 0: # Medium ticks (every 5)
|
| 67 |
+
tick_height = self.medium_tick_height
|
| 68 |
+
tick_start = 11
|
| 69 |
+
else: # Minor ticks
|
| 70 |
+
tick_height = self.minor_tick_height
|
| 71 |
+
tick_start = 13
|
| 72 |
+
|
| 73 |
+
# Create tick mark
|
| 74 |
+
tick = QGraphicsLineItem(
|
| 75 |
+
x_pos, tick_start,
|
| 76 |
+
x_pos, tick_start + tick_height
|
| 77 |
+
)
|
| 78 |
+
tick.setPen(QPen(self.ruler_color, 1))
|
| 79 |
+
self.addItem(tick)
|
| 80 |
+
|
| 81 |
+
# Create new tracker line
|
| 82 |
+
self.tracker_line = QGraphicsLineItem()
|
| 83 |
+
pen = QPen(self.ruler_color)
|
| 84 |
+
pen.setWidth(2)
|
| 85 |
+
pen.setStyle(Qt.PenStyle.SolidLine)
|
| 86 |
+
self.tracker_line.setPen(pen)
|
| 87 |
+
self.tracker_line.setZValue(100)
|
| 88 |
+
self.tracker_line.hide()
|
| 89 |
+
self.addItem(self.tracker_line)
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
self.logger.error(f"Error creating ruler: {str(e)}")
|
| 93 |
+
|
| 94 |
+
def boundingRect(self):
|
| 95 |
+
"""Return the bounding rectangle of the ruler"""
|
| 96 |
+
return self.sceneRect()
|
| 97 |
+
|
| 98 |
+
def update_tracker_position(self, x_pos):
|
| 99 |
+
"""Update the position of the tracker line"""
|
| 100 |
+
try:
|
| 101 |
+
if self.tracker_line is None:
|
| 102 |
+
# Create tracker line if it doesn't exist
|
| 103 |
+
self.tracker_line = QGraphicsLineItem()
|
| 104 |
+
pen = QPen(self.ruler_color)
|
| 105 |
+
pen.setWidth(2)
|
| 106 |
+
pen.setStyle(Qt.PenStyle.SolidLine)
|
| 107 |
+
self.tracker_line.setPen(pen)
|
| 108 |
+
self.tracker_line.setZValue(100)
|
| 109 |
+
self.addItem(self.tracker_line)
|
| 110 |
+
|
| 111 |
+
# Ensure x_pos is within bounds
|
| 112 |
+
scene_rect = self.sceneRect()
|
| 113 |
+
x_pos = max(self.strand_margin, min(x_pos, scene_rect.width() - self.strand_margin))
|
| 114 |
+
|
| 115 |
+
# Set tracker line position
|
| 116 |
+
self.tracker_line.setLine(x_pos, 0, x_pos, self.ruler_height)
|
| 117 |
+
self.tracker_line.show()
|
| 118 |
+
self.update()
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
self.logger.error(f"Error updating tracker position: {str(e)}")
|
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import QGraphicsLineItem
|
| 2 |
+
from PyQt6.QtCore import Qt, QRectF, QPointF
|
| 3 |
+
from PyQt6.QtGui import QPen, QColor, QPainter
|
| 4 |
+
|
| 5 |
+
class SequenceCursor(QGraphicsLineItem):
|
| 6 |
+
"""A vertical I-beam cursor for DNA sequence that spans between strands"""
|
| 7 |
+
|
| 8 |
+
def __init__(self, parent=None):
|
| 9 |
+
super().__init__(parent)
|
| 10 |
+
|
| 11 |
+
# Use a solid blue line
|
| 12 |
+
self.pen = QPen(QColor(0, 100, 255))
|
| 13 |
+
self.pen.setWidth(2)
|
| 14 |
+
self.pen.setStyle(Qt.PenStyle.SolidLine)
|
| 15 |
+
self.setPen(self.pen)
|
| 16 |
+
|
| 17 |
+
# Keep cursor on top
|
| 18 |
+
self.setZValue(9999)
|
| 19 |
+
|
| 20 |
+
# Don't make cursor selectable/focusable
|
| 21 |
+
self.setAcceptHoverEvents(False)
|
| 22 |
+
self.setAcceptedMouseButtons(Qt.MouseButton.NoButton)
|
| 23 |
+
|
| 24 |
+
# Disable caching for immediate updates
|
| 25 |
+
self.setCacheMode(QGraphicsLineItem.CacheMode.NoCache)
|
| 26 |
+
|
| 27 |
+
def set_position(self, x, y, height):
|
| 28 |
+
"""Position the cursor at given coordinates"""
|
| 29 |
+
self.setLine(x, y, x, y + height)
|
| 30 |
+
self.update()
|
| 31 |
+
|
| 32 |
+
def paint(self, painter, option, widget):
|
| 33 |
+
"""Draw the I-beam cursor"""
|
| 34 |
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
| 35 |
+
|
| 36 |
+
# Draw vertical line
|
| 37 |
+
painter.setPen(self.pen)
|
| 38 |
+
line = self.line()
|
| 39 |
+
painter.drawLine(line)
|
| 40 |
+
|
| 41 |
+
# Draw small horizontal lines at top and bottom using QPointF
|
| 42 |
+
bar_width = 6
|
| 43 |
+
|
| 44 |
+
# Top bar
|
| 45 |
+
top_start = QPointF(line.x1() - bar_width/2, line.y1())
|
| 46 |
+
top_end = QPointF(line.x1() + bar_width/2, line.y1())
|
| 47 |
+
painter.drawLine(top_start, top_end)
|
| 48 |
+
|
| 49 |
+
# Bottom bar
|
| 50 |
+
bottom_start = QPointF(line.x1() - bar_width/2, line.y2())
|
| 51 |
+
bottom_end = QPointF(line.x1() + bar_width/2, line.y2())
|
| 52 |
+
painter.drawLine(bottom_start, bottom_end)
|
| 53 |
+
|
| 54 |
+
def boundingRect(self):
|
| 55 |
+
"""Bounding rectangle for the cursor"""
|
| 56 |
+
line = self.line()
|
| 57 |
+
padding = 4
|
| 58 |
+
return QRectF(line.x1() - padding,
|
| 59 |
+
line.y1() - padding,
|
| 60 |
+
padding * 2,
|
| 61 |
+
line.y2() - line.y1() + padding * 2)
|
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import QGraphicsObject
|
| 2 |
+
from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QPointF, QEvent
|
| 3 |
+
from PyQt6.QtGui import QColor, QPainterPath
|
| 4 |
+
from .sequence_cursor import SequenceCursor
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
class SequenceInsertionZone(QGraphicsObject):
|
| 8 |
+
"""Handles the interactive zone between base pairs for insertion/deletion"""
|
| 9 |
+
|
| 10 |
+
insertion_point_selected = pyqtSignal(int) # Emits position when zone is clicked
|
| 11 |
+
|
| 12 |
+
def __init__(self, parent=None):
|
| 13 |
+
super().__init__(parent)
|
| 14 |
+
self.logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
self.zone_width = 8
|
| 17 |
+
|
| 18 |
+
# State tracking
|
| 19 |
+
self.hover_position = None
|
| 20 |
+
self.active_zones = [] # List of (x, y, width, height) for each zone
|
| 21 |
+
|
| 22 |
+
self.sequence_length = 0
|
| 23 |
+
self.bases_per_line = 70 # Default value
|
| 24 |
+
self.base_width = 15
|
| 25 |
+
self.strand_margin = 40
|
| 26 |
+
self.line_height = 25
|
| 27 |
+
|
| 28 |
+
self.sequence_cursor = SequenceCursor(self)
|
| 29 |
+
self.sequence_cursor.hide()
|
| 30 |
+
self.current_cursor_pos = None
|
| 31 |
+
|
| 32 |
+
self.setAcceptHoverEvents(True)
|
| 33 |
+
self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
|
| 34 |
+
self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemIsSelectable, True)
|
| 35 |
+
self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemIsFocusable, True)
|
| 36 |
+
self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemClipsToShape, False)
|
| 37 |
+
self.setZValue(100) # Keep zones above sequence
|
| 38 |
+
|
| 39 |
+
# Add tracking for visible area
|
| 40 |
+
self.visible_rect = QRectF()
|
| 41 |
+
self.scale_factor = 1.0
|
| 42 |
+
|
| 43 |
+
# Add tracking for hover state
|
| 44 |
+
self.last_valid_hover_pos = None
|
| 45 |
+
self.hover_active = False
|
| 46 |
+
|
| 47 |
+
def create_zones(self, sequence_length, base_width, strand_margin, line_height, bases_per_line, line_spacing=None):
|
| 48 |
+
"""Create insertion zones between bases"""
|
| 49 |
+
self.active_zones.clear()
|
| 50 |
+
self.line_height = line_height
|
| 51 |
+
self.line_spacing = line_spacing if line_spacing is not None else line_height * 3
|
| 52 |
+
self.bases_per_line = bases_per_line
|
| 53 |
+
self.base_width = base_width
|
| 54 |
+
self.strand_margin = strand_margin
|
| 55 |
+
self.sequence_length = sequence_length
|
| 56 |
+
self.update()
|
| 57 |
+
|
| 58 |
+
def contains(self, point):
|
| 59 |
+
"""Override contains to better handle hover detection"""
|
| 60 |
+
# Convert point to local coordinates if needed
|
| 61 |
+
local_point = point
|
| 62 |
+
if isinstance(point, QPointF):
|
| 63 |
+
local_point = self.mapFromScene(point)
|
| 64 |
+
|
| 65 |
+
# Calculate position
|
| 66 |
+
line_number = int(local_point.y() / self.line_spacing)
|
| 67 |
+
x_relative = local_point.x() - self.strand_margin
|
| 68 |
+
position_in_line = int(x_relative / self.base_width)
|
| 69 |
+
absolute_position = (line_number * self.bases_per_line) + position_in_line
|
| 70 |
+
|
| 71 |
+
# Calculate total lines
|
| 72 |
+
total_lines = (self.sequence_length + self.bases_per_line - 1) // self.bases_per_line
|
| 73 |
+
|
| 74 |
+
# Check if position is valid with more precise bounds
|
| 75 |
+
in_x_range = -self.zone_width <= x_relative <= (self.bases_per_line * self.base_width + self.zone_width)
|
| 76 |
+
in_y_range = 0 <= line_number < total_lines
|
| 77 |
+
position_valid = 0 <= absolute_position <= self.sequence_length
|
| 78 |
+
|
| 79 |
+
# Calculate vertical position within line with more generous zones
|
| 80 |
+
y_in_line = local_point.y() % self.line_spacing
|
| 81 |
+
|
| 82 |
+
# Define zones with appropriate overlap and coverage
|
| 83 |
+
zone_height = self.line_height * 1.8
|
| 84 |
+
middle_point = self.line_spacing / 2
|
| 85 |
+
|
| 86 |
+
# Upper zone covers from start to middle + overlap
|
| 87 |
+
upper_zone_start = 0
|
| 88 |
+
upper_zone_end = middle_point + (zone_height / 2)
|
| 89 |
+
|
| 90 |
+
# Lower zone covers from middle - overlap to end
|
| 91 |
+
lower_zone_start = middle_point - (zone_height / 2)
|
| 92 |
+
lower_zone_end = self.line_spacing
|
| 93 |
+
|
| 94 |
+
# Allow interaction in both upper and lower strand regions with overlap
|
| 95 |
+
upper_strand_zone = upper_zone_start <= y_in_line <= upper_zone_end
|
| 96 |
+
lower_strand_zone = lower_zone_start <= y_in_line <= lower_zone_end
|
| 97 |
+
in_vertical_zone = upper_strand_zone or lower_strand_zone
|
| 98 |
+
|
| 99 |
+
return in_x_range and in_y_range and position_valid and in_vertical_zone
|
| 100 |
+
|
| 101 |
+
def hoverMoveEvent(self, event):
|
| 102 |
+
"""Handle hover using direct coordinate calculation"""
|
| 103 |
+
pos = event.pos()
|
| 104 |
+
|
| 105 |
+
if self.contains(pos):
|
| 106 |
+
# Calculate snapped position for cursor only
|
| 107 |
+
line_number = int(pos.y() / self.line_spacing)
|
| 108 |
+
x_relative = pos.x() - self.strand_margin
|
| 109 |
+
position_in_line = int(x_relative / self.base_width)
|
| 110 |
+
absolute_position = (line_number * self.bases_per_line) + position_in_line
|
| 111 |
+
|
| 112 |
+
# Update cursor behavior
|
| 113 |
+
if self.hover_position != absolute_position:
|
| 114 |
+
self.hover_position = absolute_position
|
| 115 |
+
self.setCursor(Qt.CursorShape.IBeamCursor)
|
| 116 |
+
|
| 117 |
+
# Update ruler tracker with exact mouse position - no snapping
|
| 118 |
+
if self.scene() and self.scene().parent():
|
| 119 |
+
dna_viewer = self.scene().parent()
|
| 120 |
+
if hasattr(dna_viewer, 'ruler_scene'):
|
| 121 |
+
# Get exact mouse position in scene coordinates
|
| 122 |
+
scene_pos = self.mapToScene(pos)
|
| 123 |
+
|
| 124 |
+
# Adjust for any offset between sequence view and ruler
|
| 125 |
+
ruler_x = scene_pos.x()
|
| 126 |
+
|
| 127 |
+
# Account for margin differences between sequence and ruler
|
| 128 |
+
margin_diff = self.strand_margin - dna_viewer.ruler_scene.strand_margin
|
| 129 |
+
if margin_diff != 0:
|
| 130 |
+
ruler_x -= margin_diff
|
| 131 |
+
|
| 132 |
+
# Update tracker immediately
|
| 133 |
+
dna_viewer.ruler_scene.update_tracker_position(ruler_x)
|
| 134 |
+
|
| 135 |
+
# Force immediate updates
|
| 136 |
+
dna_viewer.ruler_scene.update()
|
| 137 |
+
dna_viewer.ruler_view.viewport().update()
|
| 138 |
+
|
| 139 |
+
# Update scene for smooth rendering
|
| 140 |
+
if self.scene():
|
| 141 |
+
self.scene().update()
|
| 142 |
+
else:
|
| 143 |
+
if self.hover_position is not None:
|
| 144 |
+
self.hover_position = None
|
| 145 |
+
self.setCursor(Qt.CursorShape.ArrowCursor)
|
| 146 |
+
|
| 147 |
+
# Hide ruler tracker when not hovering
|
| 148 |
+
if self.scene() and self.scene().parent():
|
| 149 |
+
dna_viewer = self.scene().parent()
|
| 150 |
+
if hasattr(dna_viewer, 'ruler_scene'):
|
| 151 |
+
dna_viewer.ruler_scene.tracker_line.hide()
|
| 152 |
+
dna_viewer.ruler_scene.update()
|
| 153 |
+
dna_viewer.ruler_view.viewport().update()
|
| 154 |
+
|
| 155 |
+
return super().hoverMoveEvent(event)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def mousePressEvent(self, event):
|
| 159 |
+
"""Handle mouse press to only place cursor without highlighting"""
|
| 160 |
+
pos = event.pos()
|
| 161 |
+
|
| 162 |
+
# Calculate position relative to sequence
|
| 163 |
+
x_relative = pos.x() - self.strand_margin
|
| 164 |
+
line_number = int(pos.y() / self.line_spacing)
|
| 165 |
+
|
| 166 |
+
# Calculate nearest space between bases for cursor
|
| 167 |
+
raw_position = x_relative / self.base_width
|
| 168 |
+
cursor_position = round(raw_position) # Round to nearest space
|
| 169 |
+
absolute_position = (line_number * self.bases_per_line) + cursor_position
|
| 170 |
+
|
| 171 |
+
# Confine cursor position within sequence boundaries
|
| 172 |
+
absolute_position = max(0, min(absolute_position, self.sequence_length))
|
| 173 |
+
cursor_position = absolute_position % self.bases_per_line
|
| 174 |
+
line_number = absolute_position // self.bases_per_line
|
| 175 |
+
|
| 176 |
+
# Store selection start but don't highlight yet
|
| 177 |
+
self.selection_start = absolute_position
|
| 178 |
+
|
| 179 |
+
# Reset selection state
|
| 180 |
+
self.selection_active = False
|
| 181 |
+
|
| 182 |
+
# Position cursor at nearest space between bases
|
| 183 |
+
cursor_x = self.strand_margin + (cursor_position * self.base_width)
|
| 184 |
+
cursor_y = (line_number * self.line_spacing) + (self.line_height * 0.1)
|
| 185 |
+
cursor_height = self.line_height * 2 + 5
|
| 186 |
+
|
| 187 |
+
# Show cursor
|
| 188 |
+
self.sequence_cursor.set_position(cursor_x, cursor_y, cursor_height)
|
| 189 |
+
self.sequence_cursor.show()
|
| 190 |
+
|
| 191 |
+
# Store and emit current cursor position
|
| 192 |
+
self.current_cursor_pos = absolute_position
|
| 193 |
+
|
| 194 |
+
# Get DNA feature viewer and emit cursor position change
|
| 195 |
+
if self.scene() and self.scene().parent():
|
| 196 |
+
dna_viewer = self.scene().parent()
|
| 197 |
+
sequence_viewer = dna_viewer.sequence_viewer
|
| 198 |
+
|
| 199 |
+
# Clear selection state and highlights
|
| 200 |
+
sequence_viewer.selection_start = None
|
| 201 |
+
sequence_viewer.selection_end = None
|
| 202 |
+
sequence_viewer.selection_active = False
|
| 203 |
+
|
| 204 |
+
# Clear selection highlights
|
| 205 |
+
selection_color = QColor(100, 150, 255, 100)
|
| 206 |
+
for nuc in sequence_viewer.nucleotides:
|
| 207 |
+
if nuc.highlight_color == selection_color:
|
| 208 |
+
nuc.is_highlighted = False
|
| 209 |
+
nuc.highlight_color = None
|
| 210 |
+
nuc.update()
|
| 211 |
+
|
| 212 |
+
# Emit cursor position change immediately
|
| 213 |
+
sequence_viewer.cursor_position_changed.emit(absolute_position)
|
| 214 |
+
|
| 215 |
+
event.accept()
|
| 216 |
+
|
| 217 |
+
def mouseMoveEvent(self, event):
|
| 218 |
+
"""Handle mouse drag for selection"""
|
| 219 |
+
# Only start selection if mouse has moved
|
| 220 |
+
if not self.selection_active:
|
| 221 |
+
# Check if mouse has moved enough to start selection
|
| 222 |
+
initial_pos = event.pos()
|
| 223 |
+
x_relative = initial_pos.x() - self.strand_margin
|
| 224 |
+
raw_position = x_relative / self.base_width
|
| 225 |
+
current_position = round(raw_position)
|
| 226 |
+
|
| 227 |
+
# Only activate selection if mouse has moved to a different position
|
| 228 |
+
if (current_position != self.selection_start // self.bases_per_line):
|
| 229 |
+
self.selection_active = True
|
| 230 |
+
|
| 231 |
+
if self.selection_active:
|
| 232 |
+
pos = event.pos()
|
| 233 |
+
|
| 234 |
+
# Calculate current position
|
| 235 |
+
x_relative = pos.x() - self.strand_margin
|
| 236 |
+
line_number = int(pos.y() / self.line_spacing)
|
| 237 |
+
raw_position = x_relative / self.base_width
|
| 238 |
+
cursor_position = round(raw_position) # For cursor placement
|
| 239 |
+
|
| 240 |
+
# For highlighting, use the same rounding logic as cursor
|
| 241 |
+
highlight_position = cursor_position
|
| 242 |
+
|
| 243 |
+
# Convert to sequence positions
|
| 244 |
+
current_pos = (line_number * self.bases_per_line) + highlight_position
|
| 245 |
+
|
| 246 |
+
# Confine position within sequence boundaries
|
| 247 |
+
current_pos = max(0, min(current_pos, self.sequence_length))
|
| 248 |
+
cursor_position = current_pos % self.bases_per_line
|
| 249 |
+
line_number = current_pos // self.bases_per_line
|
| 250 |
+
|
| 251 |
+
if 0 <= current_pos <= self.sequence_length:
|
| 252 |
+
scene = self.scene()
|
| 253 |
+
if scene and scene.parent():
|
| 254 |
+
dna_viewer = scene.parent()
|
| 255 |
+
sequence_viewer = dna_viewer.sequence_viewer
|
| 256 |
+
|
| 257 |
+
# Update ruler tracker with exact mouse position during selection
|
| 258 |
+
if hasattr(dna_viewer, 'ruler_scene'):
|
| 259 |
+
# Get exact mouse position in scene coordinates
|
| 260 |
+
scene_pos = self.mapToScene(pos)
|
| 261 |
+
|
| 262 |
+
# Adjust for any offset between sequence view and ruler
|
| 263 |
+
ruler_x = scene_pos.x()
|
| 264 |
+
|
| 265 |
+
# Account for margin differences between sequence and ruler
|
| 266 |
+
margin_diff = self.strand_margin - dna_viewer.ruler_scene.strand_margin
|
| 267 |
+
if margin_diff != 0:
|
| 268 |
+
ruler_x -= margin_diff
|
| 269 |
+
|
| 270 |
+
# Update tracker immediately
|
| 271 |
+
dna_viewer.ruler_scene.update_tracker_position(ruler_x)
|
| 272 |
+
dna_viewer.ruler_scene.update()
|
| 273 |
+
dna_viewer.ruler_view.viewport().update()
|
| 274 |
+
|
| 275 |
+
# Clear previous selection highlights
|
| 276 |
+
selection_color = QColor(100, 150, 255, 100)
|
| 277 |
+
for nuc in sequence_viewer.nucleotides:
|
| 278 |
+
if nuc.highlight_color == selection_color:
|
| 279 |
+
nuc.is_highlighted = False
|
| 280 |
+
nuc.highlight_color = None
|
| 281 |
+
nuc.update()
|
| 282 |
+
|
| 283 |
+
# Determine selection range
|
| 284 |
+
if current_pos >= self.selection_start:
|
| 285 |
+
start = self.selection_start
|
| 286 |
+
end = current_pos - 1
|
| 287 |
+
else:
|
| 288 |
+
start = current_pos
|
| 289 |
+
end = self.selection_start - 1
|
| 290 |
+
|
| 291 |
+
try:
|
| 292 |
+
# Only highlight if there's a valid range
|
| 293 |
+
if start <= end:
|
| 294 |
+
for base_idx in range(start, end + 1):
|
| 295 |
+
pos_strand_idx = base_idx * 2
|
| 296 |
+
neg_strand_idx = base_idx * 2 + 1
|
| 297 |
+
|
| 298 |
+
for idx in [pos_strand_idx, neg_strand_idx]:
|
| 299 |
+
if idx < len(sequence_viewer.nucleotides):
|
| 300 |
+
nuc = sequence_viewer.nucleotides[idx]
|
| 301 |
+
if not nuc.is_highlighted or nuc.highlight_color == selection_color:
|
| 302 |
+
nuc.is_highlighted = True
|
| 303 |
+
nuc.highlight_color = selection_color
|
| 304 |
+
nuc.update()
|
| 305 |
+
|
| 306 |
+
# Emit selection signal with adjusted end position for display
|
| 307 |
+
absolute_start = dna_viewer._original_start_pos + start + 1 # Add 1 only for display
|
| 308 |
+
absolute_end = dna_viewer._original_start_pos + end + 1 # Add 1 only for display
|
| 309 |
+
sequence_viewer.sequence_selected.emit(absolute_start, absolute_end)
|
| 310 |
+
|
| 311 |
+
# Update scene and cursor
|
| 312 |
+
if scene:
|
| 313 |
+
scene.update()
|
| 314 |
+
|
| 315 |
+
# Position cursor at nearest space between bases
|
| 316 |
+
cursor_x = self.strand_margin + (cursor_position * self.base_width)
|
| 317 |
+
cursor_y = (line_number * self.line_spacing) + (self.line_height * 0.1)
|
| 318 |
+
cursor_height = self.line_height * 2 + 5
|
| 319 |
+
|
| 320 |
+
self.sequence_cursor.set_position(cursor_x, cursor_y, cursor_height)
|
| 321 |
+
self.sequence_cursor.show()
|
| 322 |
+
|
| 323 |
+
self.current_cursor_pos = current_pos
|
| 324 |
+
|
| 325 |
+
except Exception as e:
|
| 326 |
+
print(f"Error highlighting: {str(e)}")
|
| 327 |
+
|
| 328 |
+
event.accept()
|
| 329 |
+
|
| 330 |
+
def mouseReleaseEvent(self, event):
|
| 331 |
+
"""Handle mouse release to end selection"""
|
| 332 |
+
if self.selection_active:
|
| 333 |
+
self.selection_active = False
|
| 334 |
+
|
| 335 |
+
# Emit the final selection position
|
| 336 |
+
if hasattr(self, 'selection_start') and self.selection_start is not None:
|
| 337 |
+
self.insertion_point_selected.emit(self.selection_start)
|
| 338 |
+
|
| 339 |
+
event.accept()
|
| 340 |
+
|
| 341 |
+
def boundingRect(self):
|
| 342 |
+
"""Return bounding rectangle for entire sequence area"""
|
| 343 |
+
if not hasattr(self, 'sequence_length') or self.sequence_length == 0:
|
| 344 |
+
return QRectF(0, 0, 1, 1) # Return minimal rect if no sequence
|
| 345 |
+
|
| 346 |
+
total_lines = (self.sequence_length + self.bases_per_line - 1) // self.bases_per_line
|
| 347 |
+
width = self.strand_margin * 2 + (self.bases_per_line * self.base_width)
|
| 348 |
+
height = total_lines * self.line_spacing
|
| 349 |
+
|
| 350 |
+
# Add padding
|
| 351 |
+
padding = 20
|
| 352 |
+
return QRectF(-padding, -padding, width + 2*padding, height + 2*padding)
|
| 353 |
+
|
| 354 |
+
def paint(self, painter, option, widget):
|
| 355 |
+
"""Paint method required by QGraphicsObject"""
|
| 356 |
+
pass
|
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import traceback
|
| 2 |
+
from PyQt6.QtWidgets import QGraphicsObject, QApplication, QGraphicsSimpleTextItem, QGraphicsLineItem
|
| 3 |
+
from PyQt6.QtCore import Qt, QRectF, pyqtSignal
|
| 4 |
+
from PyQt6.QtGui import QPen, QFont, QColor
|
| 5 |
+
from .nucleotide_item import NucleotideItem
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
class SequenceViewer(QGraphicsObject):
|
| 9 |
+
"""Component for displaying and interacting with DNA sequence"""
|
| 10 |
+
sequence_selected = pyqtSignal(int, int) # Emit start and end positions
|
| 11 |
+
cursor_position_changed = pyqtSignal(int) # Emit cursor position
|
| 12 |
+
|
| 13 |
+
def __init__(self, parent=None, logger=None):
|
| 14 |
+
super().__init__(parent)
|
| 15 |
+
|
| 16 |
+
self.logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
self._init_properties()
|
| 19 |
+
self._init_graphics_storage()
|
| 20 |
+
|
| 21 |
+
def _init_properties(self):
|
| 22 |
+
"""Initialize properties"""
|
| 23 |
+
# Layout properties
|
| 24 |
+
self.strand_margin = 40
|
| 25 |
+
self.base_width = 15
|
| 26 |
+
self.bases_per_line = 70
|
| 27 |
+
self.line_height = 25
|
| 28 |
+
self.line_spacing = 80
|
| 29 |
+
|
| 30 |
+
# Sequence properties
|
| 31 |
+
self.sequence = ""
|
| 32 |
+
self.start_pos = 0
|
| 33 |
+
|
| 34 |
+
# Selection properties
|
| 35 |
+
self.selection_start = None
|
| 36 |
+
self.selection_end = None
|
| 37 |
+
self.drag_start_pos = None
|
| 38 |
+
self.selection_active = False
|
| 39 |
+
|
| 40 |
+
# Cursor properties
|
| 41 |
+
self.cursor_pos = None
|
| 42 |
+
|
| 43 |
+
# Clipboard
|
| 44 |
+
self.clipboard = QApplication.clipboard()
|
| 45 |
+
|
| 46 |
+
# Add tracking for cleared selection highlights
|
| 47 |
+
self.cleared_selection_positions = set() # Track positions where selection was cleared
|
| 48 |
+
|
| 49 |
+
def _init_graphics_storage(self):
|
| 50 |
+
"""Initialize storage for graphics items"""
|
| 51 |
+
self.nucleotides = []
|
| 52 |
+
self.highlighted_regions = []
|
| 53 |
+
self.nucleotide_map = {'+': [], '-': []}
|
| 54 |
+
self.plot_lines = []
|
| 55 |
+
self.tick_lines = []
|
| 56 |
+
|
| 57 |
+
def set_data(self, sequence, start_pos=None):
|
| 58 |
+
"""Set sequence data and create nucleotide items"""
|
| 59 |
+
try:
|
| 60 |
+
if not sequence:
|
| 61 |
+
self.logger.warning("Empty sequence provided")
|
| 62 |
+
return
|
| 63 |
+
|
| 64 |
+
self.sequence = sequence
|
| 65 |
+
self.start_pos = start_pos if start_pos is not None else 0
|
| 66 |
+
|
| 67 |
+
# Store current highlights, excluding cleared selection highlights
|
| 68 |
+
current_highlights = []
|
| 69 |
+
selection_blue = QColor(100, 150, 255, 100)
|
| 70 |
+
|
| 71 |
+
for nuc in self.nucleotides:
|
| 72 |
+
if nuc.is_highlighted:
|
| 73 |
+
idx = self.get_nucleotide_position(nuc)
|
| 74 |
+
pos = idx // 2 # Convert to sequence position
|
| 75 |
+
|
| 76 |
+
# Only store if it's not a selection highlight that was cleared
|
| 77 |
+
if nuc.highlight_color != selection_blue or pos not in self.cleared_selection_positions:
|
| 78 |
+
current_highlights.append({
|
| 79 |
+
'position': pos,
|
| 80 |
+
'color': nuc.highlight_color
|
| 81 |
+
})
|
| 82 |
+
|
| 83 |
+
# Batch update
|
| 84 |
+
if self.scene():
|
| 85 |
+
views = self.scene().views()
|
| 86 |
+
for view in views:
|
| 87 |
+
view.setUpdatesEnabled(False)
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
# Create display
|
| 91 |
+
self._create_display()
|
| 92 |
+
|
| 93 |
+
# Reapply highlights, excluding cleared selections
|
| 94 |
+
for highlight in current_highlights:
|
| 95 |
+
pos = highlight['position']
|
| 96 |
+
color = highlight['color']
|
| 97 |
+
|
| 98 |
+
# Skip if this was a cleared selection
|
| 99 |
+
if color == selection_blue and pos in self.cleared_selection_positions:
|
| 100 |
+
continue
|
| 101 |
+
|
| 102 |
+
pos_strand_idx = pos * 2
|
| 103 |
+
neg_strand_idx = pos * 2 + 1
|
| 104 |
+
|
| 105 |
+
for idx in [pos_strand_idx, neg_strand_idx]:
|
| 106 |
+
if idx < len(self.nucleotides):
|
| 107 |
+
nuc = self.nucleotides[idx]
|
| 108 |
+
nuc.is_highlighted = True
|
| 109 |
+
nuc.highlight_color = color
|
| 110 |
+
nuc.update()
|
| 111 |
+
|
| 112 |
+
finally:
|
| 113 |
+
# Re-enable updates
|
| 114 |
+
if self.scene():
|
| 115 |
+
for view in views:
|
| 116 |
+
view.setUpdatesEnabled(True)
|
| 117 |
+
view.viewport().update()
|
| 118 |
+
|
| 119 |
+
except Exception as e:
|
| 120 |
+
self.logger.error(f"Error in set_data: {str(e)}")
|
| 121 |
+
|
| 122 |
+
def _create_display(self):
|
| 123 |
+
try:
|
| 124 |
+
# Clear existing items
|
| 125 |
+
self.cleanup_graphics()
|
| 126 |
+
|
| 127 |
+
total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
|
| 128 |
+
|
| 129 |
+
nucleotides_batch = []
|
| 130 |
+
plot_lines_batch = []
|
| 131 |
+
tick_lines_batch = []
|
| 132 |
+
position_numbers = []
|
| 133 |
+
|
| 134 |
+
current_pos = 0
|
| 135 |
+
while current_pos < len(self.sequence):
|
| 136 |
+
remaining_bases = len(self.sequence) - current_pos
|
| 137 |
+
bases_this_line = min(self.bases_per_line, remaining_bases)
|
| 138 |
+
line_text = self.sequence[current_pos:current_pos + bases_this_line]
|
| 139 |
+
|
| 140 |
+
line_num = current_pos // self.bases_per_line
|
| 141 |
+
y_pos = line_num * self.line_spacing
|
| 142 |
+
|
| 143 |
+
# Create nucleotides for this line
|
| 144 |
+
for i, nuc in enumerate(line_text):
|
| 145 |
+
x_pos = i * self.base_width + self.strand_margin
|
| 146 |
+
|
| 147 |
+
# Calculate absolute position in sequence
|
| 148 |
+
abs_pos = current_pos + i
|
| 149 |
+
|
| 150 |
+
# Determine if this nucleotide is part of padding
|
| 151 |
+
# Only consider it padding if it's at the start or end of the sequence
|
| 152 |
+
is_padding = (nuc.islower() and nuc in 'atgc' and
|
| 153 |
+
(abs_pos < 30 or abs_pos >= len(self.sequence) - 30))
|
| 154 |
+
|
| 155 |
+
# Create positive strand nucleotide
|
| 156 |
+
nuc_item = NucleotideItem(
|
| 157 |
+
nucleotide=nuc,
|
| 158 |
+
x=x_pos,
|
| 159 |
+
y=y_pos + self.line_height * 0.1,
|
| 160 |
+
width=self.base_width,
|
| 161 |
+
is_uppercase=nuc.isupper(),
|
| 162 |
+
is_padding=is_padding,
|
| 163 |
+
parent=self
|
| 164 |
+
)
|
| 165 |
+
nucleotides_batch.append(nuc_item)
|
| 166 |
+
|
| 167 |
+
# Create complement strand nucleotide
|
| 168 |
+
complement_item = NucleotideItem(
|
| 169 |
+
nucleotide=nuc,
|
| 170 |
+
x=x_pos,
|
| 171 |
+
y=y_pos + self.line_height * 1.45,
|
| 172 |
+
width=self.base_width,
|
| 173 |
+
is_uppercase=nuc.isupper(),
|
| 174 |
+
is_complement=True,
|
| 175 |
+
is_padding=is_padding,
|
| 176 |
+
parent=self
|
| 177 |
+
)
|
| 178 |
+
nucleotides_batch.append(complement_item)
|
| 179 |
+
|
| 180 |
+
# Create plot line
|
| 181 |
+
plot_y = y_pos + self.line_height * 1.15
|
| 182 |
+
plot_line = QGraphicsLineItem(
|
| 183 |
+
self.strand_margin,
|
| 184 |
+
plot_y,
|
| 185 |
+
self.strand_margin + bases_this_line * self.base_width,
|
| 186 |
+
plot_y,
|
| 187 |
+
self
|
| 188 |
+
)
|
| 189 |
+
plot_line.setPen(QPen(Qt.GlobalColor.black, 1))
|
| 190 |
+
plot_lines_batch.append(plot_line)
|
| 191 |
+
|
| 192 |
+
# Add position number at end of line
|
| 193 |
+
end_pos = str(self.start_pos + current_pos + bases_this_line)
|
| 194 |
+
pos_item = QGraphicsSimpleTextItem(end_pos, self)
|
| 195 |
+
pos_item.setFont(QFont("Courier", 12))
|
| 196 |
+
|
| 197 |
+
# Position text with fixed offset
|
| 198 |
+
extra_spacing = 25 if line_num == total_lines - 1 else 0
|
| 199 |
+
pos_x = self.strand_margin + (bases_this_line * self.base_width) + extra_spacing + 10
|
| 200 |
+
pos_y = plot_y - pos_item.boundingRect().height()/2
|
| 201 |
+
pos_item.setPos(pos_x, pos_y)
|
| 202 |
+
position_numbers.append(pos_item)
|
| 203 |
+
|
| 204 |
+
# Create tick marks
|
| 205 |
+
for i in range(bases_this_line):
|
| 206 |
+
x_pos = i * self.base_width + self.strand_margin
|
| 207 |
+
pos_1_based = current_pos + i + 1
|
| 208 |
+
|
| 209 |
+
tick_height = 12 if (i == 0 and current_pos == 0) or \
|
| 210 |
+
(i == bases_this_line - 1 and current_pos + bases_this_line == len(self.sequence)) else \
|
| 211 |
+
10 if pos_1_based % 10 == 0 else \
|
| 212 |
+
8 if pos_1_based % 5 == 0 else 5
|
| 213 |
+
|
| 214 |
+
tick = QGraphicsLineItem(
|
| 215 |
+
x_pos + self.base_width/2,
|
| 216 |
+
plot_y - tick_height/2,
|
| 217 |
+
x_pos + self.base_width/2,
|
| 218 |
+
plot_y + tick_height/2,
|
| 219 |
+
self
|
| 220 |
+
)
|
| 221 |
+
tick.setPen(QPen(Qt.GlobalColor.black, 1))
|
| 222 |
+
tick_lines_batch.append(tick)
|
| 223 |
+
|
| 224 |
+
current_pos += bases_this_line
|
| 225 |
+
|
| 226 |
+
# Add all items to scene in batches
|
| 227 |
+
self.nucleotides = nucleotides_batch
|
| 228 |
+
self.plot_lines = plot_lines_batch
|
| 229 |
+
self.tick_lines = tick_lines_batch
|
| 230 |
+
|
| 231 |
+
# Add strand indicators and position numbers
|
| 232 |
+
self._add_strand_indicators(total_lines)
|
| 233 |
+
|
| 234 |
+
except Exception as e:
|
| 235 |
+
self.logger.error(f"Error in create optimized display: {str(e)}")
|
| 236 |
+
|
| 237 |
+
def _add_strand_indicators(self, total_lines):
|
| 238 |
+
# Add first line indicators
|
| 239 |
+
five_prime_pos = QGraphicsSimpleTextItem("5'", self)
|
| 240 |
+
five_prime_pos.setFont(QFont("Arial", 10))
|
| 241 |
+
five_prime_pos.setPos(0, self.line_height * 0.26)
|
| 242 |
+
|
| 243 |
+
three_prime_neg = QGraphicsSimpleTextItem("3'", self)
|
| 244 |
+
three_prime_neg.setFont(QFont("Arial", 10))
|
| 245 |
+
three_prime_neg.setPos(0, self.line_height * 1.58)
|
| 246 |
+
|
| 247 |
+
# Add last line indicators
|
| 248 |
+
last_line_width = (len(self.sequence) % self.bases_per_line) * self.base_width
|
| 249 |
+
if last_line_width == 0:
|
| 250 |
+
last_line_width = self.bases_per_line * self.base_width
|
| 251 |
+
|
| 252 |
+
three_prime_pos = QGraphicsSimpleTextItem("3'", self)
|
| 253 |
+
three_prime_pos.setFont(QFont("Arial", 10))
|
| 254 |
+
three_prime_pos.setPos(
|
| 255 |
+
last_line_width + self.strand_margin + 20,
|
| 256 |
+
(total_lines - 1) * self.line_spacing + self.line_height * 0.26
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
five_prime_neg = QGraphicsSimpleTextItem("5'", self)
|
| 260 |
+
five_prime_neg.setFont(QFont("Arial", 10))
|
| 261 |
+
five_prime_neg.setPos(
|
| 262 |
+
last_line_width + self.strand_margin + 20,
|
| 263 |
+
(total_lines - 1) * self.line_spacing + self.line_height * 1.58
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
def cleanup_graphics(self):
|
| 267 |
+
"""Clean up all graphics items"""
|
| 268 |
+
try:
|
| 269 |
+
# Remove plot lines
|
| 270 |
+
for line in self.plot_lines:
|
| 271 |
+
if line.scene():
|
| 272 |
+
line.scene().removeItem(line)
|
| 273 |
+
self.plot_lines.clear()
|
| 274 |
+
|
| 275 |
+
# Remove tick lines
|
| 276 |
+
for line in self.tick_lines:
|
| 277 |
+
if line.scene():
|
| 278 |
+
line.scene().removeItem(line)
|
| 279 |
+
self.tick_lines.clear()
|
| 280 |
+
|
| 281 |
+
# Remove nucleotides
|
| 282 |
+
for nuc in self.nucleotides:
|
| 283 |
+
if nuc.scene():
|
| 284 |
+
nuc.scene().removeItem(nuc)
|
| 285 |
+
self.nucleotides.clear()
|
| 286 |
+
|
| 287 |
+
# Remove all text items (including position numbers)
|
| 288 |
+
for item in self.childItems():
|
| 289 |
+
if isinstance(item, QGraphicsSimpleTextItem):
|
| 290 |
+
if item.scene():
|
| 291 |
+
item.scene().removeItem(item)
|
| 292 |
+
|
| 293 |
+
# Clear nucleotide maps but preserve highlights
|
| 294 |
+
self.nucleotide_map['+'].clear()
|
| 295 |
+
self.nucleotide_map['-'].clear()
|
| 296 |
+
|
| 297 |
+
self.logger.debug("Cleaned up all graphics items")
|
| 298 |
+
|
| 299 |
+
except Exception as e:
|
| 300 |
+
self.logger.error(f"Error in cleanup_graphics: {str(e)}")
|
| 301 |
+
|
| 302 |
+
def highlight_sequence(self, start_pos, end_pos, color, strand='+'):
|
| 303 |
+
"""Highlight sequence with proper strand handling"""
|
| 304 |
+
try:
|
| 305 |
+
# Store highlight information
|
| 306 |
+
self.highlighted_regions.append((start_pos, end_pos, color, strand))
|
| 307 |
+
|
| 308 |
+
# Calculate which lines contain the sequence
|
| 309 |
+
start_line = start_pos // self.bases_per_line
|
| 310 |
+
end_line = end_pos // self.bases_per_line
|
| 311 |
+
|
| 312 |
+
# Calculate positions within lines
|
| 313 |
+
start_pos_in_line = start_pos % self.bases_per_line
|
| 314 |
+
end_pos_in_line = end_pos % self.bases_per_line
|
| 315 |
+
|
| 316 |
+
self.logger.debug(f"Highlighting from line {start_line} to {end_line}")
|
| 317 |
+
self.logger.debug(f"Start pos in line: {start_pos_in_line}, End pos in line: {end_pos_in_line}")
|
| 318 |
+
|
| 319 |
+
# For each line that contains part of the sequence
|
| 320 |
+
for line_num in range(start_line, end_line + 1):
|
| 321 |
+
# Calculate start and end positions for this line
|
| 322 |
+
line_start = start_pos_in_line if line_num == start_line else 0
|
| 323 |
+
line_end = end_pos_in_line if line_num == end_line else self.bases_per_line - 1
|
| 324 |
+
|
| 325 |
+
# Calculate base indices for this line
|
| 326 |
+
base_start = line_num * self.bases_per_line + line_start
|
| 327 |
+
base_end = line_num * self.bases_per_line + line_end
|
| 328 |
+
|
| 329 |
+
# Highlight nucleotides
|
| 330 |
+
for i in range(base_start, base_end + 1):
|
| 331 |
+
if i >= len(self.nucleotides):
|
| 332 |
+
break
|
| 333 |
+
|
| 334 |
+
# For negative strand, highlight both strands' nucleotides
|
| 335 |
+
if strand == '-':
|
| 336 |
+
# Highlight negative strand nucleotide
|
| 337 |
+
neg_idx = i * 2 + 1 # Odd indices for negative strand
|
| 338 |
+
if neg_idx < len(self.nucleotides):
|
| 339 |
+
nuc = self.nucleotides[neg_idx]
|
| 340 |
+
nuc.is_highlighted = True
|
| 341 |
+
nuc.highlight_color = color
|
| 342 |
+
nuc.update()
|
| 343 |
+
else:
|
| 344 |
+
# Highlight positive strand nucleotide
|
| 345 |
+
pos_idx = i * 2 # Even indices for positive strand
|
| 346 |
+
if pos_idx < len(self.nucleotides):
|
| 347 |
+
nuc = self.nucleotides[pos_idx]
|
| 348 |
+
nuc.is_highlighted = True
|
| 349 |
+
nuc.highlight_color = color
|
| 350 |
+
nuc.update()
|
| 351 |
+
|
| 352 |
+
# Force scene update
|
| 353 |
+
if self.scene():
|
| 354 |
+
self.scene().update()
|
| 355 |
+
|
| 356 |
+
self.logger.debug(f"Highlighted sequence on strand {strand} from {start_pos} to {end_pos}")
|
| 357 |
+
|
| 358 |
+
except Exception as e:
|
| 359 |
+
self.logger.error(f"Error in highlight_sequence: {str(e)}")
|
| 360 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 361 |
+
|
| 362 |
+
def clear_highlights(self):
|
| 363 |
+
"""Clear all highlights with optimized rendering"""
|
| 364 |
+
try:
|
| 365 |
+
# Get view and disable updates
|
| 366 |
+
view = None
|
| 367 |
+
if self.scene():
|
| 368 |
+
views = self.scene().views()
|
| 369 |
+
if views:
|
| 370 |
+
view = views[0]
|
| 371 |
+
view.setUpdatesEnabled(False)
|
| 372 |
+
|
| 373 |
+
try:
|
| 374 |
+
self.highlighted_regions.clear()
|
| 375 |
+
|
| 376 |
+
# Batch collect nucleotides that need updating
|
| 377 |
+
nucleotides_to_update = []
|
| 378 |
+
|
| 379 |
+
for nuc in self.nucleotides:
|
| 380 |
+
if nuc.is_highlighted:
|
| 381 |
+
nuc.is_highlighted = False
|
| 382 |
+
nuc.highlight_color = None
|
| 383 |
+
nucleotides_to_update.append(nuc)
|
| 384 |
+
|
| 385 |
+
# Update all nucleotides in a single batch
|
| 386 |
+
if nucleotides_to_update:
|
| 387 |
+
# Use prepareGeometryChange for better performance
|
| 388 |
+
for nuc in nucleotides_to_update:
|
| 389 |
+
nuc.prepareGeometryChange()
|
| 390 |
+
|
| 391 |
+
# Force a single scene update
|
| 392 |
+
if self.scene():
|
| 393 |
+
self.scene().update()
|
| 394 |
+
|
| 395 |
+
finally:
|
| 396 |
+
# Re-enable view updates
|
| 397 |
+
if view:
|
| 398 |
+
view.setUpdatesEnabled(True)
|
| 399 |
+
view.viewport().update()
|
| 400 |
+
|
| 401 |
+
except Exception as e:
|
| 402 |
+
self.logger.error(f"Error in clear_highlights: {str(e)}")
|
| 403 |
+
# Make sure view updates are re-enabled
|
| 404 |
+
if self.scene():
|
| 405 |
+
views = self.scene().views()
|
| 406 |
+
if views:
|
| 407 |
+
views[0].setUpdatesEnabled(True)
|
| 408 |
+
views[0].viewport().update()
|
| 409 |
+
|
| 410 |
+
def get_nucleotide_position(self, nucleotide):
|
| 411 |
+
"""Get the position of a nucleotide in the sequence"""
|
| 412 |
+
try:
|
| 413 |
+
idx = self.nucleotides.index(nucleotide)
|
| 414 |
+
return idx
|
| 415 |
+
except ValueError:
|
| 416 |
+
return -1
|
| 417 |
+
|
| 418 |
+
def boundingRect(self):
|
| 419 |
+
"""Return the bounding rectangle"""
|
| 420 |
+
if not self.sequence:
|
| 421 |
+
return QRectF(0, 0, 100, 100) # Return a default size when empty
|
| 422 |
+
|
| 423 |
+
# Calculate total width including margins
|
| 424 |
+
width = (self.base_width * self.bases_per_line) + (self.strand_margin * 2)
|
| 425 |
+
|
| 426 |
+
# Calculate height using line spacing
|
| 427 |
+
total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
|
| 428 |
+
height = total_lines * self.line_spacing
|
| 429 |
+
|
| 430 |
+
# Add some padding
|
| 431 |
+
height += 50
|
| 432 |
+
width += 50
|
| 433 |
+
|
| 434 |
+
return QRectF(0, 0, width, height)
|
| 435 |
+
|
| 436 |
+
def paint(self, painter, option, widget):
|
| 437 |
+
"""Paint method required by QGraphicsObject"""
|
| 438 |
+
pass
|
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene,
|
| 2 |
+
QLabel, QFrame)
|
| 3 |
+
from PyQt6.QtCore import Qt, pyqtSignal
|
| 4 |
+
from PyQt6.QtGui import QBrush, QColor
|
| 5 |
+
from .components.sequence_viewer import SequenceViewer
|
| 6 |
+
from .components.feature_viewer import FeatureViewer
|
| 7 |
+
from .components.ruler import Ruler
|
| 8 |
+
from .components.sequence_insertion_zone import SequenceInsertionZone
|
| 9 |
+
import logging
|
| 10 |
+
import traceback
|
| 11 |
+
|
| 12 |
+
class DNAFeatureViewer(QWidget):
|
| 13 |
+
"""Main widget for displaying DNA sequences"""
|
| 14 |
+
sequence_selected = pyqtSignal(int, int) # Emit start and end positions
|
| 15 |
+
|
| 16 |
+
def __init__(self, parent=None):
|
| 17 |
+
super().__init__(parent)
|
| 18 |
+
|
| 19 |
+
# Get logger from parent or global settings
|
| 20 |
+
if parent and hasattr(parent, 'logger'):
|
| 21 |
+
self.logger = parent.logger
|
| 22 |
+
else:
|
| 23 |
+
self.logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
# Create layout
|
| 26 |
+
self.layout = QVBoxLayout(self)
|
| 27 |
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
| 28 |
+
self.layout.setSpacing(0)
|
| 29 |
+
|
| 30 |
+
# Initialize components
|
| 31 |
+
self._init_views()
|
| 32 |
+
self._init_components()
|
| 33 |
+
self._init_status_panel()
|
| 34 |
+
self._init_connections()
|
| 35 |
+
|
| 36 |
+
# Set focus policy for the widget itself
|
| 37 |
+
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
| 38 |
+
self.setFocus()
|
| 39 |
+
|
| 40 |
+
def _init_views(self):
|
| 41 |
+
"""Initialize graphics views"""
|
| 42 |
+
# Create ruler view
|
| 43 |
+
self.ruler_view = QGraphicsView()
|
| 44 |
+
self.ruler_scene = Ruler()
|
| 45 |
+
self.ruler_view.setScene(self.ruler_scene)
|
| 46 |
+
self.ruler_view.setFixedHeight(25)
|
| 47 |
+
self.ruler_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
| 48 |
+
self.ruler_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
| 49 |
+
self.ruler_view.setViewportMargins(0, 0, 0, 0)
|
| 50 |
+
self.ruler_view.setFrameStyle(0)
|
| 51 |
+
self.ruler_view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
| 52 |
+
self.ruler_view.setEnabled(False) # Disable all user interaction with ruler
|
| 53 |
+
|
| 54 |
+
# Set background color for both view and viewport
|
| 55 |
+
background_color = QColor(240, 240, 240)
|
| 56 |
+
self.ruler_view.setBackgroundBrush(QBrush(background_color))
|
| 57 |
+
self.ruler_view.viewport().setStyleSheet(f"background-color: rgb({background_color.red()}, {background_color.green()}, {background_color.blue()})")
|
| 58 |
+
self.ruler_view.setAutoFillBackground(True)
|
| 59 |
+
|
| 60 |
+
# Create main view with proper event handling
|
| 61 |
+
self.view = QGraphicsView()
|
| 62 |
+
self.scene = QGraphicsScene(self)
|
| 63 |
+
self.view.setScene(self.scene)
|
| 64 |
+
|
| 65 |
+
# Add these lines to remove the frame from main view
|
| 66 |
+
self.view.setFrameStyle(0) # Remove frame
|
| 67 |
+
self.view.setViewportMargins(0, 0, 0, 0) # Remove margins
|
| 68 |
+
|
| 69 |
+
# Set alignment to force left anchoring
|
| 70 |
+
self.view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
| 71 |
+
|
| 72 |
+
# Make sure scene events are handled
|
| 73 |
+
self.scene.setItemIndexMethod(QGraphicsScene.ItemIndexMethod.NoIndex)
|
| 74 |
+
|
| 75 |
+
def _init_components(self):
|
| 76 |
+
try:
|
| 77 |
+
# Create sequence and feature viewers
|
| 78 |
+
if not hasattr(self, 'sequence_viewer'):
|
| 79 |
+
self.sequence_viewer = SequenceViewer(logger=self.logger)
|
| 80 |
+
# Reduce left margin/padding
|
| 81 |
+
self.sequence_viewer.strand_margin = 20
|
| 82 |
+
self.scene.addItem(self.sequence_viewer)
|
| 83 |
+
|
| 84 |
+
if not hasattr(self, 'feature_viewer'):
|
| 85 |
+
# Create feature viewer and add to scene
|
| 86 |
+
self.feature_viewer = FeatureViewer()
|
| 87 |
+
# Match margin with sequence viewer
|
| 88 |
+
self.feature_viewer.strand_margin = 20
|
| 89 |
+
self.scene.addItem(self.feature_viewer)
|
| 90 |
+
|
| 91 |
+
# Add sequence insertion zone
|
| 92 |
+
if not hasattr(self, 'insertion_zone'):
|
| 93 |
+
self.insertion_zone = SequenceInsertionZone()
|
| 94 |
+
# Match margin with sequence viewer
|
| 95 |
+
self.insertion_zone.strand_margin = 20
|
| 96 |
+
self.scene.addItem(self.insertion_zone)
|
| 97 |
+
|
| 98 |
+
# Add views to layout
|
| 99 |
+
self.layout.insertWidget(0, self.ruler_view)
|
| 100 |
+
self.layout.addWidget(self.view)
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
self.logger.error(f"Error in _init_components: {str(e)}")
|
| 104 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 105 |
+
|
| 106 |
+
def _init_status_panel(self):
|
| 107 |
+
"""Initialize status panel"""
|
| 108 |
+
self.status_panel = QLabel()
|
| 109 |
+
self.status_panel.setFrameStyle(QFrame.Shape.Panel | QFrame.Shadow.Sunken)
|
| 110 |
+
self.status_panel.setStyleSheet("""
|
| 111 |
+
QLabel {
|
| 112 |
+
background-color: #f0f0f0;
|
| 113 |
+
padding: 5px;
|
| 114 |
+
border-top: 1px solid #ccc;
|
| 115 |
+
min-height: 20px;
|
| 116 |
+
}
|
| 117 |
+
""")
|
| 118 |
+
self.layout.addWidget(self.status_panel)
|
| 119 |
+
|
| 120 |
+
def _init_connections(self):
|
| 121 |
+
"""Initialize signal connections"""
|
| 122 |
+
# Connect sequence viewer signals
|
| 123 |
+
self.sequence_viewer.sequence_selected.connect(self._on_sequence_selected)
|
| 124 |
+
self.sequence_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
|
| 125 |
+
|
| 126 |
+
# Connect feature viewer signals
|
| 127 |
+
self.feature_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
|
| 128 |
+
|
| 129 |
+
# Add viewport resize handling
|
| 130 |
+
self.view.viewport().installEventFilter(self)
|
| 131 |
+
|
| 132 |
+
def set_data(self, sequence, features=None, start_pos=None):
|
| 133 |
+
"""Set sequence and feature data"""
|
| 134 |
+
try:
|
| 135 |
+
if start_pos is None:
|
| 136 |
+
start_pos = 0
|
| 137 |
+
|
| 138 |
+
# self.logger.debug(f"Features: {features[:2] if features else None}")
|
| 139 |
+
|
| 140 |
+
# Store original start position for status panel
|
| 141 |
+
self._original_start_pos = start_pos
|
| 142 |
+
|
| 143 |
+
# Update components
|
| 144 |
+
self.sequence_viewer.set_data(sequence, start_pos)
|
| 145 |
+
if features is not None:
|
| 146 |
+
self.feature_viewer.set_data(sequence, features, start_pos)
|
| 147 |
+
|
| 148 |
+
# Update insertion zone
|
| 149 |
+
self.insertion_zone.create_zones(
|
| 150 |
+
sequence_length=len(sequence),
|
| 151 |
+
base_width=self.sequence_viewer.base_width,
|
| 152 |
+
strand_margin=self.sequence_viewer.strand_margin,
|
| 153 |
+
line_height=self.sequence_viewer.line_height,
|
| 154 |
+
bases_per_line=self.sequence_viewer.bases_per_line,
|
| 155 |
+
line_spacing=self.sequence_viewer.line_spacing
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Position components
|
| 159 |
+
self.feature_viewer.setY(0)
|
| 160 |
+
self.insertion_zone.setY(0)
|
| 161 |
+
|
| 162 |
+
# Update scene rect
|
| 163 |
+
combined_rect = self.sequence_viewer.boundingRect().united(
|
| 164 |
+
self.feature_viewer.boundingRect()
|
| 165 |
+
).united(
|
| 166 |
+
self.insertion_zone.boundingRect()
|
| 167 |
+
)
|
| 168 |
+
self.scene.setSceneRect(combined_rect)
|
| 169 |
+
|
| 170 |
+
# Update ruler
|
| 171 |
+
self.ruler_scene.create_ruler(self.sequence_viewer.bases_per_line)
|
| 172 |
+
|
| 173 |
+
# Update status panel with original gene positions
|
| 174 |
+
sequence_length = len(sequence) if sequence else 0
|
| 175 |
+
self.status_panel.setText(
|
| 176 |
+
f"Showing: {self._original_start_pos}...{self._original_start_pos + sequence_length} = {sequence_length} bp"
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
self.update()
|
| 180 |
+
self.logger.debug("Finished setting data")
|
| 181 |
+
|
| 182 |
+
except Exception as e:
|
| 183 |
+
self.logger.error(f"Error setting data: {str(e)}")
|
| 184 |
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
| 185 |
+
|
| 186 |
+
def _on_sequence_selected(self, start_pos, end_pos):
|
| 187 |
+
"""Handle sequence selection"""
|
| 188 |
+
try:
|
| 189 |
+
# Calculate selected length
|
| 190 |
+
selected_length = end_pos - start_pos + 1
|
| 191 |
+
|
| 192 |
+
# Update only the status panel
|
| 193 |
+
self.status_panel.setText(f"Selected: {start_pos}...{end_pos} = {selected_length} bp")
|
| 194 |
+
|
| 195 |
+
# Emit signal without updating line edits
|
| 196 |
+
self.sequence_selected.emit(start_pos, end_pos)
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
self.logger.error(f"Error handling sequence selection: {str(e)}")
|
| 200 |
+
|
| 201 |
+
def _on_cursor_position_changed(self, position):
|
| 202 |
+
"""Handle cursor position changes"""
|
| 203 |
+
try:
|
| 204 |
+
if position >= 0:
|
| 205 |
+
# Convert position to be relative to gene's actual start position
|
| 206 |
+
absolute_position = self._original_start_pos + position
|
| 207 |
+
self.status_panel.setText(f"Insertion Point: {absolute_position}")
|
| 208 |
+
self.logger.debug(f"Cursor position changed to absolute position: {absolute_position}")
|
| 209 |
+
else:
|
| 210 |
+
# Reset to showing current sequence range
|
| 211 |
+
if hasattr(self, 'sequence_viewer'):
|
| 212 |
+
sequence = self.sequence_viewer.sequence
|
| 213 |
+
sequence_length = len(sequence)
|
| 214 |
+
self.status_panel.setText(
|
| 215 |
+
f"Showing: {self._original_start_pos}...{self._original_start_pos + sequence_length} = {sequence_length} bp"
|
| 216 |
+
)
|
| 217 |
+
except Exception as e:
|
| 218 |
+
self.logger.error(f"Error handling cursor position change: {str(e)}")
|
| 219 |
+
|
| 220 |
+
def eventFilter(self, obj, event):
|
| 221 |
+
"""Handle viewport resize events"""
|
| 222 |
+
if obj == self.view.viewport() and event.type() == event.Type.Resize:
|
| 223 |
+
try:
|
| 224 |
+
viewport_width = event.size().width()
|
| 225 |
+
margin = 100
|
| 226 |
+
|
| 227 |
+
available_width = viewport_width - margin
|
| 228 |
+
base_width = self.sequence_viewer.base_width
|
| 229 |
+
|
| 230 |
+
max_bases = (available_width // base_width)
|
| 231 |
+
new_bases = (max_bases // 10) * 10
|
| 232 |
+
new_bases = max(10, min(new_bases, 200))
|
| 233 |
+
|
| 234 |
+
if new_bases != self.sequence_viewer.bases_per_line:
|
| 235 |
+
# Store current sequence and features
|
| 236 |
+
current_sequence = self.sequence_viewer.sequence
|
| 237 |
+
current_start = self.sequence_viewer.start_pos
|
| 238 |
+
|
| 239 |
+
# Store only guide highlights (red/green), not selection highlights (blue)
|
| 240 |
+
current_highlights = []
|
| 241 |
+
selection_blue = QColor(100, 150, 255, 100)
|
| 242 |
+
|
| 243 |
+
for nuc in self.sequence_viewer.nucleotides:
|
| 244 |
+
if nuc.is_highlighted and nuc.highlight_color != selection_blue:
|
| 245 |
+
idx = self.sequence_viewer.get_nucleotide_position(nuc)
|
| 246 |
+
current_highlights.append({
|
| 247 |
+
'position': idx // 2,
|
| 248 |
+
'color': nuc.highlight_color
|
| 249 |
+
})
|
| 250 |
+
|
| 251 |
+
# Store cursor position
|
| 252 |
+
cursor_sequence_pos = None
|
| 253 |
+
if hasattr(self.insertion_zone, 'current_cursor_pos'):
|
| 254 |
+
cursor_sequence_pos = self.insertion_zone.current_cursor_pos
|
| 255 |
+
|
| 256 |
+
try:
|
| 257 |
+
# Update bases per line
|
| 258 |
+
self.sequence_viewer.bases_per_line = new_bases
|
| 259 |
+
self.feature_viewer.bases_per_line = new_bases
|
| 260 |
+
|
| 261 |
+
# Update components
|
| 262 |
+
self.set_data(current_sequence, None, current_start)
|
| 263 |
+
|
| 264 |
+
# Reapply only guide highlights
|
| 265 |
+
for highlight in current_highlights:
|
| 266 |
+
pos = highlight['position']
|
| 267 |
+
pos_strand_idx = pos * 2
|
| 268 |
+
neg_strand_idx = pos * 2 + 1
|
| 269 |
+
|
| 270 |
+
for idx in [pos_strand_idx, neg_strand_idx]:
|
| 271 |
+
if idx < len(self.sequence_viewer.nucleotides):
|
| 272 |
+
nuc = self.sequence_viewer.nucleotides[idx]
|
| 273 |
+
nuc.is_highlighted = True
|
| 274 |
+
nuc.highlight_color = highlight['color']
|
| 275 |
+
nuc.update()
|
| 276 |
+
|
| 277 |
+
# Restore cursor position
|
| 278 |
+
if cursor_sequence_pos is not None:
|
| 279 |
+
# Calculate new visual position based on sequence position
|
| 280 |
+
line_number = cursor_sequence_pos // new_bases
|
| 281 |
+
pos_in_line = cursor_sequence_pos % new_bases
|
| 282 |
+
|
| 283 |
+
# Calculate exact pixel coordinates for cursor
|
| 284 |
+
cursor_x = self.sequence_viewer.strand_margin + (pos_in_line * self.sequence_viewer.base_width)
|
| 285 |
+
cursor_y = (line_number * self.sequence_viewer.line_spacing) + (self.sequence_viewer.line_height * 0.1)
|
| 286 |
+
cursor_height = self.sequence_viewer.line_height * 2 + 5
|
| 287 |
+
|
| 288 |
+
# Update cursor position
|
| 289 |
+
if hasattr(self.insertion_zone, 'sequence_cursor'):
|
| 290 |
+
self.insertion_zone.sequence_cursor.set_position(cursor_x, cursor_y, cursor_height)
|
| 291 |
+
self.insertion_zone.sequence_cursor.show()
|
| 292 |
+
self.insertion_zone.current_cursor_pos = cursor_sequence_pos
|
| 293 |
+
|
| 294 |
+
# Update ruler
|
| 295 |
+
self.ruler_scene.create_ruler(new_bases)
|
| 296 |
+
|
| 297 |
+
# Update scroll positions
|
| 298 |
+
scroll_value = self.view.horizontalScrollBar().value()
|
| 299 |
+
self.ruler_view.horizontalScrollBar().setValue(scroll_value)
|
| 300 |
+
|
| 301 |
+
finally:
|
| 302 |
+
# Force immediate update
|
| 303 |
+
self.view.viewport().update()
|
| 304 |
+
self.ruler_view.viewport().update()
|
| 305 |
+
|
| 306 |
+
except Exception as e:
|
| 307 |
+
self.logger.error(f"Error handling resize event: {str(e)}")
|
| 308 |
+
|
| 309 |
+
return super().eventFilter(obj, event)
|